common fields
This commit is contained in:
@@ -16,8 +16,9 @@ import {
|
||||
} from "react-router-dom";
|
||||
|
||||
import { ConfigContext } from "./providers/ConfigContext";
|
||||
import ProfileView from "./components/ProfileView";
|
||||
|
||||
function Dashboard({ basePath }: { basePath: string }) {
|
||||
function DefaultDashboard({ basePath }: { basePath: string }) {
|
||||
const config = React.useContext(ConfigContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -32,7 +33,6 @@ function Dashboard({ basePath }: { basePath: string }) {
|
||||
<Typography variant="body1" sx={{ color: 'text.secondary' }}>
|
||||
Select a resource from the sidebar to manage data.
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
@@ -42,10 +42,10 @@ function Dashboard({ basePath }: { basePath: string }) {
|
||||
}}
|
||||
>
|
||||
{visibleResources.map((res) => (
|
||||
<Paper
|
||||
key={res.name}
|
||||
sx={{
|
||||
p: 3,
|
||||
<Paper
|
||||
key={res.name}
|
||||
sx={{
|
||||
p: 3,
|
||||
textAlign: "center",
|
||||
cursor: 'pointer',
|
||||
transition: 'transform 0.2s',
|
||||
@@ -62,9 +62,15 @@ function Dashboard({ basePath }: { basePath: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
import ProfileView from "./components/ProfileView";
|
||||
interface AdminAppProps {
|
||||
basePath: string;
|
||||
fieldComponents: FieldComponents;
|
||||
Dashboard?: React.ComponentType<{ basePath: string }>;
|
||||
Layout?: React.ComponentType<AdminLayoutProps>;
|
||||
LoginPage?: React.ComponentType<any>;
|
||||
}
|
||||
|
||||
function AdminApp({ basePath, fieldComponents }: { basePath: string; fieldComponents?: FieldComponents }) {
|
||||
function AdminApp({ basePath, fieldComponents, Dashboard = DefaultDashboard, Layout = AdminLayout, LoginPage = AuthPage }: AdminAppProps) {
|
||||
const { currentUser, login, logout, loading, error } = useAuth();
|
||||
const config = React.useContext(ConfigContext);
|
||||
const navigate = useNavigate();
|
||||
@@ -74,10 +80,10 @@ function AdminApp({ basePath, fieldComponents }: { basePath: string; fieldCompon
|
||||
|
||||
if (!currentUser) {
|
||||
return (
|
||||
<AuthPage
|
||||
<LoginPage
|
||||
mode="login"
|
||||
login={login}
|
||||
register={async () => {}} // Disable registration for Admin
|
||||
register={async () => {}}
|
||||
loading={loading}
|
||||
error={error}
|
||||
onSwitchMode={() => {}}
|
||||
@@ -88,7 +94,7 @@ function AdminApp({ basePath, fieldComponents }: { basePath: string; fieldCompon
|
||||
}
|
||||
|
||||
return (
|
||||
<AdminLayout
|
||||
<Layout
|
||||
username={currentUser.username}
|
||||
onLogout={logout}
|
||||
onSelectResource={(name) => navigate(`/admin/${name}`)}
|
||||
@@ -102,11 +108,11 @@ function AdminApp({ basePath, fieldComponents }: { basePath: string; fieldCompon
|
||||
<Route path="/:resourceName/create" element={<ResourceRouteWrapper fieldComponents={fieldComponents} />} />
|
||||
<Route path="/:resourceName/edit/:id" element={<ResourceRouteWrapper fieldComponents={fieldComponents} />} />
|
||||
</Routes>
|
||||
</AdminLayout>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
function ResourceRouteWrapper({ fieldComponents }: { fieldComponents?: FieldComponents }) {
|
||||
function ResourceRouteWrapper({ fieldComponents }: { fieldComponents: FieldComponents }) {
|
||||
const { resourceName } = useParams();
|
||||
const config = React.useContext(ConfigContext);
|
||||
const selectedResource = config?.resources.find((r) => r.name === resourceName);
|
||||
@@ -116,14 +122,25 @@ function ResourceRouteWrapper({ fieldComponents }: { fieldComponents?: FieldComp
|
||||
return <ResourceView config={selectedResource} fieldComponents={fieldComponents} />;
|
||||
}
|
||||
|
||||
interface AdminLayoutProps {
|
||||
children: React.ReactNode;
|
||||
onSelectResource: (resourceName: string | null) => void;
|
||||
onLogout: () => void;
|
||||
username?: string;
|
||||
resources: import("./types/config").ResourceConfig[];
|
||||
}
|
||||
|
||||
interface AdminProps {
|
||||
basePath?: string;
|
||||
resourceOverrides?: Record<string, any>;
|
||||
profileConfig?: any;
|
||||
fieldComponents?: FieldComponents;
|
||||
fieldComponents: FieldComponents;
|
||||
Dashboard?: React.ComponentType<{ basePath: string }>;
|
||||
Layout?: React.ComponentType<AdminLayoutProps>;
|
||||
LoginPage?: React.ComponentType<any>;
|
||||
}
|
||||
|
||||
export default function Admin({ basePath = "/admin", resourceOverrides = {}, profileConfig = {}, fieldComponents = {} }: AdminProps) {
|
||||
export default function Admin({ basePath = "/admin", resourceOverrides = {}, profileConfig = {}, fieldComponents, Dashboard, Layout, LoginPage }: AdminProps) {
|
||||
const existingConfig = React.useContext(ConfigContext);
|
||||
const [config, setConfig] = React.useState<AppConfig | null>(existingConfig);
|
||||
|
||||
@@ -153,16 +170,14 @@ export default function Admin({ basePath = "/admin", resourceOverrides = {}, pro
|
||||
|
||||
const content = (
|
||||
<UploadProvider>
|
||||
<AdminApp basePath={basePath} fieldComponents={fieldComponents} />
|
||||
<AdminApp basePath={basePath} fieldComponents={fieldComponents} Dashboard={Dashboard} Layout={Layout} LoginPage={LoginPage} />
|
||||
</UploadProvider>
|
||||
);
|
||||
|
||||
// If we have an existing config, we are already inside a Provider and QueryClient
|
||||
if (existingConfig) {
|
||||
return content;
|
||||
}
|
||||
|
||||
// Fallback for standalone usage
|
||||
return (
|
||||
<ConfigContext.Provider value={config}>
|
||||
{content}
|
||||
|
||||
@@ -31,6 +31,7 @@ import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ResourceConfig } from '../types/config';
|
||||
import { EnhancedTableComponents } from '../types/overrides';
|
||||
import { getFieldOptions, toGridValueOptions, resolveTemplate } from '../utils/options';
|
||||
|
||||
interface EnhancedTableProps {
|
||||
@@ -44,6 +45,7 @@ interface EnhancedTableProps {
|
||||
onDelete: (id: string) => void;
|
||||
onCreate: () => void;
|
||||
onNavigateToResource?: (resourceName: string, id: string) => void;
|
||||
components?: EnhancedTableComponents;
|
||||
}
|
||||
|
||||
export default function EnhancedTable({
|
||||
@@ -57,6 +59,7 @@ export default function EnhancedTable({
|
||||
onDelete,
|
||||
onCreate,
|
||||
onNavigateToResource,
|
||||
components: tableComponents,
|
||||
}: EnhancedTableProps) {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
@@ -85,7 +88,7 @@ export default function EnhancedTable({
|
||||
type: muiType,
|
||||
flex: 1,
|
||||
minWidth: 150,
|
||||
renderCell: (params: GridRenderCellParams) => <FieldRenderer params={params} field={field} fieldKey={key} config={config} onNavigate={onNavigateToResource} navigate={navigate} />
|
||||
renderCell: (params: GridRenderCellParams) => <FieldRenderer params={params} field={field} fieldKey={key} config={config} onNavigate={onNavigateToResource} navigate={navigate} components={tableComponents} />
|
||||
};
|
||||
|
||||
if (muiType === 'date' || muiType === 'dateTime') {
|
||||
@@ -158,6 +161,7 @@ export default function EnhancedTable({
|
||||
onDelete={onDelete}
|
||||
onNavigate={onNavigateToResource}
|
||||
navigate={navigate}
|
||||
components={tableComponents}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
@@ -225,7 +229,7 @@ export default function EnhancedTable({
|
||||
);
|
||||
}
|
||||
|
||||
function MobileCardRow({ row, config, onDelete, onNavigate, navigate }: any) {
|
||||
function MobileCardRow({ row, config, onDelete, onNavigate, navigate, components }: any) {
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const id = row[config.primaryKey];
|
||||
@@ -261,7 +265,7 @@ function MobileCardRow({ row, config, onDelete, onNavigate, navigate }: any) {
|
||||
{field.label}
|
||||
</Typography>
|
||||
<Typography variant="body2" component="div" sx={{ fontWeight: 500, wordBreak: 'break-all' }}>
|
||||
<FieldRenderer params={{ value: row[key], row }} field={field} fieldKey={key} config={config} onNavigate={onNavigate} navigate={navigate} isMobile />
|
||||
<FieldRenderer params={{ value: row[key], row }} field={field} fieldKey={key} config={config} onNavigate={onNavigate} navigate={navigate} isMobile components={components} />
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
@@ -289,12 +293,17 @@ function getFormattedDisplayValue(item: any, displayField?: string | string[], e
|
||||
return item[displayField] || item.id || JSON.stringify(item);
|
||||
}
|
||||
|
||||
function FieldRenderer({ params, field, fieldKey, config, onNavigate, navigate, isMobile }: any) {
|
||||
function FieldRenderer({ params, field, fieldKey, config, onNavigate, navigate, isMobile, components }: any) {
|
||||
const value = params.value;
|
||||
const isPk = fieldKey === config.primaryKey;
|
||||
|
||||
if (field.formatter) return field.formatter(value);
|
||||
|
||||
const customRenderer = components?.cellRenderers?.[field.type as string];
|
||||
if (customRenderer) {
|
||||
return React.createElement(customRenderer, { value, row: params.row, field, fieldKey, config, onNavigate, isMobile });
|
||||
}
|
||||
|
||||
// 1. Single Relation
|
||||
if (field.relation && value && !Array.isArray(value)) {
|
||||
const relationId = typeof value === 'object' ? (value.id || value._id || value.pk) : value;
|
||||
|
||||
@@ -11,9 +11,10 @@ import {
|
||||
import DoneIcon from "@mui/icons-material/Done";
|
||||
import FilterListIcon from "@mui/icons-material/FilterList";
|
||||
import { ResourceField, ResourceMode } from "../types/config";
|
||||
import { FilterBarComponents } from "../types/overrides";
|
||||
import { getFieldOptions, resolveTemplate } from "../utils/options";
|
||||
|
||||
function FilterAutocomplete({
|
||||
export function FilterAutocomplete({
|
||||
options,
|
||||
value,
|
||||
label,
|
||||
@@ -160,8 +161,14 @@ function renderFilterInput(
|
||||
field: ResourceField,
|
||||
options: string[],
|
||||
value: any,
|
||||
onChange: (key: string, val: any) => void
|
||||
onChange: (key: string, val: any) => void,
|
||||
components?: FilterBarComponents,
|
||||
) {
|
||||
const CustomInput = components?.filterInputs?.[fieldName];
|
||||
if (CustomInput) {
|
||||
return <CustomInput field={field} value={value} onChange={(val) => onChange("value", val)} options={options} />;
|
||||
}
|
||||
|
||||
const filterType = field.filterType;
|
||||
|
||||
if (filterType === "number-range") {
|
||||
@@ -208,6 +215,7 @@ export interface FilterBarProps {
|
||||
appliedValues: Record<string, any>;
|
||||
onApply: (values: Record<string, any>) => void;
|
||||
onClear: () => void;
|
||||
components?: FilterBarComponents;
|
||||
}
|
||||
|
||||
export default function FilterBar({
|
||||
@@ -217,6 +225,7 @@ export default function FilterBar({
|
||||
appliedValues,
|
||||
onApply,
|
||||
onClear,
|
||||
components: filterComponents,
|
||||
}: FilterBarProps) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [draft, setDraft] = React.useState<Record<string, any>>(() => ({ ...appliedValues }));
|
||||
@@ -294,7 +303,7 @@ export default function FilterBar({
|
||||
{field.label}
|
||||
</Box>
|
||||
{renderFilterInput(fieldName, field, options, raw, (key, val) =>
|
||||
updateDraft(fieldName, key, val)
|
||||
updateDraft(fieldName, key, val), filterComponents
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -22,7 +22,7 @@ interface GenericFormProps {
|
||||
loading?: boolean;
|
||||
readOnly?: boolean;
|
||||
onEditClick?: () => void;
|
||||
fieldComponents?: FieldComponents;
|
||||
fieldComponents: FieldComponents;
|
||||
}
|
||||
|
||||
export default function GenericForm({
|
||||
@@ -57,7 +57,7 @@ export default function GenericForm({
|
||||
queries: allRelations.map(relName => {
|
||||
const relatedRes = appConfig?.resources.find(r => r.name === relName);
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { getListQueryOptions } = useResource(relatedRes!);
|
||||
const { getListQueryOptions } = useResource(relatedRes!, { fieldComponents });
|
||||
return {
|
||||
...getListQueryOptions(),
|
||||
enabled: !!relatedRes,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Box, Typography, Paper, CircularProgress, Alert } from '@mui/material';
|
||||
import { useResource } from '../hooks/useResource';
|
||||
import GenericForm from './GenericForm';
|
||||
import { ConfigContext } from '../providers/ConfigContext';
|
||||
import { defaultFieldComponents } from './fields/DefaultFieldComponents';
|
||||
|
||||
export default function ProfileView() {
|
||||
const appConfig = React.useContext(ConfigContext);
|
||||
@@ -13,11 +14,10 @@ export default function ProfileView() {
|
||||
return <Alert severity="error">Profile configuration not found.</Alert>;
|
||||
}
|
||||
|
||||
// Create a modified config where only extraFields are editable
|
||||
const editableConfig = React.useMemo(() => {
|
||||
const newFields = { ...resourceConfig.fields };
|
||||
const extraFields = profileConfig.extraFields || [];
|
||||
|
||||
|
||||
Object.keys(newFields).forEach(key => {
|
||||
newFields[key] = {
|
||||
...newFields[key],
|
||||
@@ -31,13 +31,12 @@ export default function ProfileView() {
|
||||
};
|
||||
}, [resourceConfig, profileConfig.extraFields]);
|
||||
|
||||
const { useMe, useUpdateMe } = useResource(resourceConfig);
|
||||
const { useMe, useUpdateMe } = useResource(resourceConfig, { fieldComponents: defaultFieldComponents });
|
||||
const { data: profile, isLoading, error } = useMe();
|
||||
const updateMutation = useUpdateMe();
|
||||
|
||||
const handleSave = async (formData: any) => {
|
||||
try {
|
||||
// Only send editable fields to prevent accidental overwrites of read-only data
|
||||
const extraFields = profileConfig.extraFields || [];
|
||||
const dataToSave = Object.keys(formData)
|
||||
.filter(key => extraFields.includes(key))
|
||||
@@ -76,6 +75,7 @@ export default function ProfileView() {
|
||||
onSave={handleSave}
|
||||
onCancel={() => window.history.back()}
|
||||
loading={updateMutation.isPending}
|
||||
fieldComponents={defaultFieldComponents}
|
||||
/>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
@@ -5,7 +5,6 @@ import type { ResourceField } from '../types/config';
|
||||
import { FieldComponents } from '../types/overrides';
|
||||
import { useResource } from '../hooks/useResource';
|
||||
import { resolveTemplate } from '../utils/options';
|
||||
import GenericForm from './GenericForm';
|
||||
import EnhancedTable from './EnhancedTable';
|
||||
import FilterBar from './FilterBar';
|
||||
import { useParams, useLocation, useNavigate } from 'react-router-dom';
|
||||
@@ -13,7 +12,7 @@ import { useParams, useLocation, useNavigate } from 'react-router-dom';
|
||||
interface ResourceViewProps {
|
||||
config: ResourceConfig;
|
||||
onNavigateToResource?: (resourceName: string, id: string) => void;
|
||||
fieldComponents?: FieldComponents;
|
||||
fieldComponents: FieldComponents;
|
||||
}
|
||||
|
||||
import { GridPaginationModel } from '@mui/x-data-grid';
|
||||
@@ -117,7 +116,7 @@ export default function ResourceView({ config, onNavigateToResource, fieldCompon
|
||||
|
||||
const [appliedFilters, setAppliedFilters] = React.useState<Record<string, any>>({});
|
||||
|
||||
const { useList, useRead, useCreate, useUpdate, useDelete } = useResource(config);
|
||||
const { useList, useRead, useCreate, useUpdate, useDelete, components } = useResource(config, { fieldComponents });
|
||||
|
||||
const queryParams = React.useMemo(() => {
|
||||
if (!isServer) return { limit: 10000 };
|
||||
@@ -202,7 +201,7 @@ export default function ResourceView({ config, onNavigateToResource, fieldCompon
|
||||
</Box>
|
||||
) : (
|
||||
<Paper sx={{ p: 4 }}>
|
||||
<GenericForm
|
||||
<components.GenericForm
|
||||
config={config}
|
||||
initialData={isCreate ? null : itemQuery.data}
|
||||
onSave={handleSave}
|
||||
@@ -210,7 +209,6 @@ export default function ResourceView({ config, onNavigateToResource, fieldCompon
|
||||
loading={createMutation.isPending || updateMutation.isPending}
|
||||
readOnly={isView}
|
||||
onEditClick={() => navigate(`/admin/${config.name}/edit/${id}`)}
|
||||
fieldComponents={fieldComponents}
|
||||
/>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
@@ -6,6 +6,7 @@ import DateField from './DateField';
|
||||
import EnumField from './EnumField';
|
||||
import RelationField from './RelationField';
|
||||
import ImageUploadField from './ImageUploadField';
|
||||
import FallbackField from './FallbackField';
|
||||
|
||||
export const defaultFieldComponents: FieldComponents = {
|
||||
string: TextFieldEntry,
|
||||
@@ -17,4 +18,5 @@ export const defaultFieldComponents: FieldComponents = {
|
||||
enum: EnumField,
|
||||
image: ImageUploadField,
|
||||
relation: RelationField,
|
||||
default: FallbackField,
|
||||
};
|
||||
|
||||
13
react-openapi/components/fields/FallbackField.tsx
Normal file
13
react-openapi/components/fields/FallbackField.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { TextField } from '@mui/material';
|
||||
import { FieldComponentProps } from '../../types/overrides';
|
||||
|
||||
export default function FallbackField({ field, value }: FieldComponentProps) {
|
||||
return (
|
||||
<TextField
|
||||
fullWidth
|
||||
label={field.label}
|
||||
value={typeof value === 'object' ? JSON.stringify(value) : value || ''}
|
||||
disabled
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import { TextField as MuiTextField } from '@mui/material';
|
||||
import { ResourceField } from '../../types/config';
|
||||
import { FieldComponentProps, FieldComponents } from '../../types/overrides';
|
||||
import { defaultFieldComponents } from './DefaultFieldComponents';
|
||||
import ObjectField from './ObjectField';
|
||||
import ImageUploadField from './ImageUploadField';
|
||||
|
||||
interface FormFieldProps {
|
||||
export interface FormFieldProps {
|
||||
name: string;
|
||||
field: ResourceField;
|
||||
value: any;
|
||||
@@ -16,18 +13,7 @@ interface FormFieldProps {
|
||||
uploading: boolean;
|
||||
baseUrl: string;
|
||||
relationDataMap?: Record<string, any[]>;
|
||||
components?: FieldComponents;
|
||||
}
|
||||
|
||||
function FallbackField({ field, value }: FieldComponentProps) {
|
||||
return (
|
||||
<MuiTextField
|
||||
fullWidth
|
||||
label={field.label}
|
||||
value={typeof value === 'object' ? JSON.stringify(value) : value || ''}
|
||||
disabled
|
||||
/>
|
||||
);
|
||||
components: FieldComponents;
|
||||
}
|
||||
|
||||
export default function FormField({
|
||||
@@ -40,13 +26,8 @@ export default function FormField({
|
||||
uploading,
|
||||
baseUrl,
|
||||
relationDataMap = {},
|
||||
components: componentsProp,
|
||||
components,
|
||||
}: FormFieldProps) {
|
||||
const components = React.useMemo(
|
||||
() => ({ ...defaultFieldComponents, ...componentsProp }),
|
||||
[componentsProp],
|
||||
);
|
||||
|
||||
const fieldProps: FieldComponentProps = {
|
||||
name,
|
||||
field,
|
||||
@@ -59,6 +40,8 @@ export default function FormField({
|
||||
uploading,
|
||||
};
|
||||
|
||||
const childComponents = components;
|
||||
|
||||
// 1. Object (recursive) - requires parent FormField for recursion
|
||||
if (field.type === 'object' && field.schema && !field.relation) {
|
||||
const renderChild = (childProps: FieldComponentProps) => (
|
||||
@@ -72,7 +55,7 @@ export default function FormField({
|
||||
uploading={childProps.uploading!}
|
||||
baseUrl={childProps.baseUrl!}
|
||||
relationDataMap={childProps.relationDataMap}
|
||||
components={componentsProp}
|
||||
components={components}
|
||||
/>
|
||||
);
|
||||
return <ObjectField {...fieldProps} renderField={renderChild} />;
|
||||
@@ -80,22 +63,23 @@ export default function FormField({
|
||||
|
||||
// 2. Image
|
||||
if (field.type === 'image') {
|
||||
const ImageField = components.image || ImageUploadField;
|
||||
const ImageField = components.image;
|
||||
if (!ImageField) return null;
|
||||
return <ImageField {...fieldProps} />;
|
||||
}
|
||||
|
||||
// 3. Relation
|
||||
if (field.relation && relationDataMap[field.relation]) {
|
||||
const RelationFieldComp = components.relation || defaultFieldComponents.relation!;
|
||||
const RelationFieldComp = components.relation;
|
||||
if (!RelationFieldComp) return null;
|
||||
return <RelationFieldComp {...fieldProps} />;
|
||||
}
|
||||
|
||||
// 4. Lookup by field type
|
||||
const Component = components[field.type];
|
||||
const Component = components[field.type] || components.default;
|
||||
if (Component) {
|
||||
return <Component {...fieldProps} />;
|
||||
}
|
||||
|
||||
// 5. Fallback for unknown types
|
||||
return <FallbackField {...fieldProps} />;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -7,5 +7,6 @@ export { default as DateField } from './DateField';
|
||||
export { default as EnumField } from './EnumField';
|
||||
export { default as RelationField } from './RelationField';
|
||||
export { default as ObjectField } from './ObjectField';
|
||||
export { default as FallbackField } from './FallbackField';
|
||||
export { defaultFieldComponents } from './DefaultFieldComponents';
|
||||
export type { ObjectFieldProps } from './ObjectField';
|
||||
|
||||
@@ -1,31 +1,44 @@
|
||||
import { useQuery, useMutation, useQueryClient, keepPreviousData } from "@tanstack/react-query";
|
||||
import * as React from "react";
|
||||
import { api } from "../api/client";
|
||||
import { ResourceConfig } from "../types/config";
|
||||
import { ConfigContext } from "../providers/ConfigContext";
|
||||
import * as React from "react";
|
||||
|
||||
import { FieldComponents } from "../types/overrides";
|
||||
import { FieldComponents, FieldComponentProps } from "../types/overrides";
|
||||
import { defaultFieldComponents } from "../components/fields/DefaultFieldComponents";
|
||||
import FormField from "../components/fields/FormField";
|
||||
import GenericForm from "../components/GenericForm";
|
||||
|
||||
export function useResource<T = any>(config: ResourceConfig | undefined, options?: { fieldComponents?: FieldComponents }) {
|
||||
function wrapFormField(merged: FieldComponents) {
|
||||
return (props: Omit<React.ComponentProps<typeof FormField>, 'components'>) =>
|
||||
<FormField {...props} components={merged} />;
|
||||
}
|
||||
|
||||
function wrapGenericForm(merged: FieldComponents) {
|
||||
return (props: Omit<React.ComponentProps<typeof GenericForm>, 'fieldComponents'>) =>
|
||||
<GenericForm {...props} fieldComponents={merged} />;
|
||||
}
|
||||
|
||||
export function useResource<T = any>(config: ResourceConfig | undefined, options?: { fieldComponents: FieldComponents }) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Return empty/disabled hooks if config is missing
|
||||
|
||||
const { name = '', endpoint = '', primaryKey = 'id' } = config || {};
|
||||
|
||||
const mergedComponents = React.useMemo(
|
||||
() => options?.fieldComponents ? ({ ...defaultFieldComponents, ...options.fieldComponents }) : undefined,
|
||||
[options?.fieldComponents],
|
||||
);
|
||||
|
||||
// --- READ ALL ---
|
||||
const useList = (params?: any) =>
|
||||
const useList = (params?: any) =>
|
||||
useQuery({
|
||||
queryKey: [name, "list", params],
|
||||
queryFn: async () => {
|
||||
if (!endpoint) return { data: [], total: 0 };
|
||||
console.log('params:', params);
|
||||
// @ts-ignore
|
||||
const res = await api.get<T[]>(endpoint, { params });
|
||||
const total = res.headers ? parseInt(res.headers['x-total-count'] || res.headers['X-Total-Count']) : undefined;
|
||||
return {
|
||||
data: res.data,
|
||||
total: isNaN(total as any) ? undefined : total
|
||||
return {
|
||||
data: res.data,
|
||||
total: isNaN(total as any) ? undefined : total
|
||||
};
|
||||
},
|
||||
enabled: !!endpoint,
|
||||
@@ -38,7 +51,6 @@ export function useResource<T = any>(config: ResourceConfig | undefined, options
|
||||
queryKey: [name, "detail", id, params],
|
||||
queryFn: async () => {
|
||||
if (!id || !endpoint) return null;
|
||||
// @ts-ignore
|
||||
const res = await api.get<T>(`${endpoint}/${id}`, params ? { params } : undefined);
|
||||
return res.data;
|
||||
},
|
||||
@@ -50,7 +62,6 @@ export function useResource<T = any>(config: ResourceConfig | undefined, options
|
||||
useMutation({
|
||||
mutationFn: async (data: Partial<T>) => {
|
||||
if (!endpoint) throw new Error("Endpoint not defined");
|
||||
// @ts-ignore
|
||||
const res = await api.post<T>(endpoint, data);
|
||||
return res.data;
|
||||
},
|
||||
@@ -64,12 +75,10 @@ export function useResource<T = any>(config: ResourceConfig | undefined, options
|
||||
useMutation({
|
||||
mutationFn: async ({ id, data }: { id: string; data: Partial<T> }) => {
|
||||
if (!endpoint) throw new Error("Endpoint not defined");
|
||||
// @ts-ignore
|
||||
const res = await api.put<T>(`${endpoint}/${id}`, data);
|
||||
return res.data;
|
||||
},
|
||||
onSuccess: (updatedItem) => {
|
||||
// @ts-ignore
|
||||
const id = updatedItem[primaryKey];
|
||||
queryClient.invalidateQueries({ queryKey: [name, "list"] });
|
||||
queryClient.invalidateQueries({ queryKey: [name, "detail", id] });
|
||||
@@ -81,15 +90,13 @@ export function useResource<T = any>(config: ResourceConfig | undefined, options
|
||||
useMutation({
|
||||
mutationFn: async ({ id, data }: { id: string; data: Partial<T> }) => {
|
||||
if (!endpoint) throw new Error("Endpoint not defined");
|
||||
// @ts-ignore
|
||||
const res = await api.patch<T>(`${endpoint}/${id}`, data);
|
||||
return res.data;
|
||||
},
|
||||
onSuccess: (updatedItem) => {
|
||||
// @ts-ignore
|
||||
const id = updatedItem[primaryKey];
|
||||
const listId = updatedItem[primaryKey];
|
||||
queryClient.invalidateQueries({ queryKey: [name, "list"] });
|
||||
queryClient.invalidateQueries({ queryKey: [name, "detail", id] });
|
||||
queryClient.invalidateQueries({ queryKey: [name, "detail", listId] });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -111,12 +118,11 @@ export function useResource<T = any>(config: ResourceConfig | undefined, options
|
||||
queryKey: [name, "list", params],
|
||||
queryFn: async () => {
|
||||
if (!endpoint) return { data: [], total: 0 };
|
||||
// @ts-ignore
|
||||
const res = await api.get<T[]>(endpoint, { params });
|
||||
const total = res.headers ? parseInt(res.headers['x-total-count'] || res.headers['X-Total-Count']) : undefined;
|
||||
return {
|
||||
data: res.data,
|
||||
total: isNaN(total as any) ? undefined : total
|
||||
return {
|
||||
data: res.data,
|
||||
total: isNaN(total as any) ? undefined : total
|
||||
};
|
||||
},
|
||||
enabled: !!endpoint,
|
||||
@@ -128,7 +134,6 @@ export function useResource<T = any>(config: ResourceConfig | undefined, options
|
||||
queryKey: [name, "me"],
|
||||
queryFn: async () => {
|
||||
if (!endpoint) return null;
|
||||
// @ts-ignore
|
||||
const res = await api.get<T>(`${endpoint}/me`);
|
||||
return res.data;
|
||||
},
|
||||
@@ -140,7 +145,6 @@ export function useResource<T = any>(config: ResourceConfig | undefined, options
|
||||
useMutation({
|
||||
mutationFn: async (data: Partial<T>) => {
|
||||
if (!endpoint) throw new Error("Endpoint not defined");
|
||||
// @ts-ignore
|
||||
const res = await api.put<T>(`${endpoint}/me`, data);
|
||||
return res.data;
|
||||
},
|
||||
@@ -150,10 +154,14 @@ export function useResource<T = any>(config: ResourceConfig | undefined, options
|
||||
},
|
||||
});
|
||||
|
||||
const components = {
|
||||
...defaultFieldComponents,
|
||||
...options?.fieldComponents,
|
||||
};
|
||||
const components = React.useMemo(() => {
|
||||
if (!mergedComponents) return undefined;
|
||||
return {
|
||||
...mergedComponents,
|
||||
FormField: wrapFormField(mergedComponents),
|
||||
GenericForm: wrapGenericForm(mergedComponents),
|
||||
};
|
||||
}, [mergedComponents]);
|
||||
|
||||
return {
|
||||
useList,
|
||||
@@ -169,9 +177,8 @@ export function useResource<T = any>(config: ResourceConfig | undefined, options
|
||||
};
|
||||
}
|
||||
|
||||
export function useResourceByName<T = any>(name: string, options?: { fieldComponents?: FieldComponents }) {
|
||||
export function useResourceByName<T = any>(name: string, options?: { fieldComponents: FieldComponents }) {
|
||||
const config = React.useContext(ConfigContext);
|
||||
const resourceConfig = config?.resources.find((r) => r.name === name);
|
||||
return useResource<T>(resourceConfig, options);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,20 +2,12 @@ export { default as Admin } from "./Admin";
|
||||
export { api, auth, initializeApiClients } from "./api/client";
|
||||
export { getAppConfig } from "./config";
|
||||
export type { AppConfig, ResourceConfig, ResourceField, ResourceMode } from "./types/config";
|
||||
export type { FieldComponents, FieldComponentProps, FieldComponent, FieldOverride, ResourceOverride } from "./types/overrides";
|
||||
export type { FieldComponents, FieldComponentProps, FieldComponent, FieldOverride, ResourceOverride, EnhancedTableComponents, FilterBarComponents, CellRendererProps, CellRenderer } from "./types/overrides";
|
||||
export { AppProvider } from "./providers/AppProvider";
|
||||
export { ConfigContext, useConfig } from "./providers/ConfigContext";
|
||||
export { useResource, useResourceByName } from "./hooks/useResource";
|
||||
export { default as FilterBar } from "./components/FilterBar";
|
||||
export {
|
||||
defaultFieldComponents,
|
||||
FormField,
|
||||
TextField,
|
||||
NumberField,
|
||||
BooleanField,
|
||||
DateField,
|
||||
EnumField,
|
||||
RelationField,
|
||||
ObjectField,
|
||||
ImageUploadField,
|
||||
} from "./components/fields";
|
||||
export { default as FilterBar, FilterAutocomplete } from "./components/FilterBar";
|
||||
export { default as EnhancedTable } from "./components/EnhancedTable";
|
||||
export { default as GenericForm } from "./components/GenericForm";
|
||||
export { default as ResourceView } from "./components/ResourceView";
|
||||
export { defaultFieldComponents, FormField, TextField, NumberField, BooleanField, DateField, EnumField, RelationField, ObjectField, ImageUploadField, FallbackField } from "./components/fields";
|
||||
|
||||
@@ -42,4 +42,32 @@ export type FieldComponent = React.ComponentType<FieldComponentProps>;
|
||||
export type FieldComponents = Partial<Record<FieldType, FieldComponent>> & {
|
||||
relation?: FieldComponent;
|
||||
image?: FieldComponent;
|
||||
default?: FieldComponent;
|
||||
};
|
||||
|
||||
export interface CellRendererProps {
|
||||
value: any;
|
||||
row: any;
|
||||
field: ResourceField;
|
||||
fieldKey: string;
|
||||
config: import('./config').ResourceConfig;
|
||||
onNavigate?: (resourceName: string, id: string) => void;
|
||||
isMobile?: boolean;
|
||||
}
|
||||
|
||||
export type CellRenderer = React.ComponentType<CellRendererProps>;
|
||||
|
||||
export interface EnhancedTableComponents {
|
||||
cellRenderers?: Partial<Record<FieldType, CellRenderer>>;
|
||||
}
|
||||
|
||||
export interface FilterBarComponents {
|
||||
filterInputs?: Record<string, React.ComponentType<{
|
||||
field: ResourceField;
|
||||
value: any;
|
||||
onChange: (val: any) => void;
|
||||
options: string[];
|
||||
}>>;
|
||||
}
|
||||
|
||||
export type { FieldType };
|
||||
|
||||
@@ -26,8 +26,6 @@ import PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
||||
import RemoveCircleOutlineIcon from "@mui/icons-material/RemoveCircleOutline";
|
||||
import FiberManualRecordIcon from "@mui/icons-material/FiberManualRecord";
|
||||
import {
|
||||
useFetchRequest,
|
||||
useUpdateFetchRequest,
|
||||
useFetchRequestAmbiguities,
|
||||
useResolveAmbiguity,
|
||||
} from "./features/fetch-requests";
|
||||
@@ -37,7 +35,7 @@ import type {
|
||||
ProgressMessage,
|
||||
} from "./features/fetch-requests";
|
||||
import { RETRY_MAX, formatApiError } from "./features/fetch-requests";
|
||||
import { useConfig } from "../react-openapi";
|
||||
import { useResourceByName, useConfig, defaultFieldComponents } from "../react-openapi";
|
||||
|
||||
const statusColors: Record<FetchRequestStatus, "default" | "primary" | "warning" | "info" | "success" | "error"> = {
|
||||
pending: "default",
|
||||
@@ -148,8 +146,9 @@ export default function FetchRequestDetail() {
|
||||
const navigate = useNavigate();
|
||||
const config = useConfig();
|
||||
|
||||
const { data: fetchRequest, isLoading, error: fetchError, refetch: refetchRequest } = useFetchRequest(id!);
|
||||
const updateMutation = useUpdateFetchRequest();
|
||||
const { useRead, usePatch } = useResourceByName("fetch-requests", { fieldComponents: defaultFieldComponents });
|
||||
const { data: fetchRequest, isLoading, error: fetchError, refetch: refetchRequest } = useRead(id!);
|
||||
const updateMutation = usePatch();
|
||||
const resolveMutation = useResolveAmbiguity();
|
||||
const { data: ambiguities, refetch: refetchAmbiguities } = useFetchRequestAmbiguities(id!);
|
||||
|
||||
|
||||
@@ -4,16 +4,9 @@ import {
|
||||
Container,
|
||||
Paper,
|
||||
Typography,
|
||||
TextField,
|
||||
Button,
|
||||
ToggleButtonGroup,
|
||||
ToggleButton,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Chip,
|
||||
IconButton,
|
||||
CircularProgress,
|
||||
@@ -25,6 +18,7 @@ import {
|
||||
DialogContentText,
|
||||
DialogActions,
|
||||
Tooltip,
|
||||
TextField,
|
||||
Select,
|
||||
MenuItem,
|
||||
InputLabel,
|
||||
@@ -43,10 +37,6 @@ import ScheduleIcon from "@mui/icons-material/Schedule";
|
||||
import HourglassEmptyIcon from "@mui/icons-material/HourglassEmpty";
|
||||
import WarningAmberIcon from "@mui/icons-material/WarningAmber";
|
||||
import {
|
||||
useFetchRequestsList,
|
||||
useCreateFetchRequest,
|
||||
useUpdateFetchRequest,
|
||||
useDeleteFetchRequest,
|
||||
useUploadFile,
|
||||
} from "./features/fetch-requests";
|
||||
import type {
|
||||
@@ -57,7 +47,8 @@ import type {
|
||||
} from "./features/fetch-requests";
|
||||
import { RETRY_MAX, formatApiError } from "./features/fetch-requests";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useResourceByName, useConfig } from "../react-openapi";
|
||||
import { useResourceByName, useConfig, defaultFieldComponents } from "../react-openapi";
|
||||
import type { ResourceField } from "../react-openapi";
|
||||
|
||||
const statusColors: Record<FetchRequestStatus, "default" | "primary" | "warning" | "info" | "success" | "error"> = {
|
||||
pending: "default",
|
||||
@@ -85,14 +76,14 @@ function formatDate(iso: string) {
|
||||
}
|
||||
|
||||
function formatDateRange(start?: string, end?: string) {
|
||||
if (!start && !end) return "—";
|
||||
if (!start && !end) return "\u2014";
|
||||
const s = start ? new Date(start).toLocaleDateString() : "?";
|
||||
const e = end ? new Date(end).toLocaleDateString() : "?";
|
||||
return `${s} → ${e}`;
|
||||
return `${s} \u2192 ${e}`;
|
||||
}
|
||||
|
||||
function shortId(fp: string) {
|
||||
return fp.length > 8 ? fp.slice(0, 8) + "…" : fp;
|
||||
return fp.length > 8 ? fp.slice(0, 8) + "\u2026" : fp;
|
||||
}
|
||||
|
||||
export default function FetchRequests() {
|
||||
@@ -116,11 +107,13 @@ export default function FetchRequests() {
|
||||
const [accountFilter, setAccountFilter] = React.useState("");
|
||||
const [sourceFilter, setSourceFilter] = React.useState<"all" | "file" | "email">("all");
|
||||
|
||||
const { data: listData, isLoading, isFetching, refetch } = useFetchRequestsList({
|
||||
const { useList, useCreate, usePatch, useDelete, components } = useResourceByName("fetch-requests", { fieldComponents: defaultFieldComponents });
|
||||
const { data: listData, isLoading, isFetching, refetch } = useList({
|
||||
...(statusFilter.length > 0 ? { status: statusFilter.join(",") } : {}),
|
||||
...(accountFilter ? { account_name: accountFilter } : {}),
|
||||
...(sourceFilter !== "all" ? { source_type: sourceFilter } : {}),
|
||||
});
|
||||
|
||||
const { useList: useAccountsList } = useResourceByName("accounts");
|
||||
const { data: accountsData } = useAccountsList();
|
||||
const accountOptions: string[] = React.useMemo(() => {
|
||||
@@ -129,11 +122,15 @@ export default function FetchRequests() {
|
||||
|
||||
const config = useConfig();
|
||||
const fetchRes = config?.resources.find((r: any) => r.name === "fetch-requests");
|
||||
const formatOptions: string[] = fetchRes?.fields?.source?.schema?.format?.options as string[] ?? [];
|
||||
const formatField: ResourceField | undefined = fetchRes?.fields?.source?.schema?.fields?.format;
|
||||
const formatOptions: string[] = formatField?.options ?? formatField?.schema?.options as string[] ?? [];
|
||||
const startDateField: ResourceField | undefined = fetchRes?.fields?.start_date;
|
||||
const endDateField: ResourceField | undefined = fetchRes?.fields?.end_date;
|
||||
const payorUsernameField: ResourceField | undefined = fetchRes?.fields?.payor_username;
|
||||
|
||||
const createMutation = useCreateFetchRequest();
|
||||
const updateMutation = useUpdateFetchRequest();
|
||||
const deleteMutation = useDeleteFetchRequest();
|
||||
const createMutation = useCreate();
|
||||
const updateMutation = usePatch();
|
||||
const deleteMutation = useDelete();
|
||||
const uploadMutation = useUploadFile();
|
||||
|
||||
const requests = listData?.data ?? [];
|
||||
@@ -178,7 +175,7 @@ export default function FetchRequests() {
|
||||
navigate(`/fetch-requests/${result.id}`);
|
||||
} catch (err: any) {
|
||||
if (err?.response?.status === 409) {
|
||||
setSnackbar({ message: "Duplicate — same fingerprint already exists", severity: "error" });
|
||||
setSnackbar({ message: "Duplicate \u2014 same fingerprint already exists", severity: "error" });
|
||||
} else {
|
||||
setSnackbar({ message: formatApiError(err) || "Failed to create fetch request", severity: "error" });
|
||||
}
|
||||
@@ -265,25 +262,43 @@ export default function FetchRequests() {
|
||||
Uploaded as: {uploadedPath}
|
||||
</Alert>
|
||||
)}
|
||||
<FormControl size="small">
|
||||
<InputLabel>Format</InputLabel>
|
||||
<Select value={format} onChange={(e) => setFormat(e.target.value)} label="Format">
|
||||
{formatOptions.map((opt) => (
|
||||
<MenuItem key={opt} value={opt}>{opt}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
{formatField && components?.FormField ? (
|
||||
<components.FormField
|
||||
name="format"
|
||||
field={formatField}
|
||||
value={format}
|
||||
onChange={setFormat}
|
||||
/>
|
||||
) : (
|
||||
<FormControl size="small">
|
||||
<InputLabel>Format</InputLabel>
|
||||
<Select value={format} onChange={(e) => setFormat(e.target.value)} label="Format">
|
||||
{formatOptions.map((opt) => (
|
||||
<MenuItem key={opt} value={opt}>{opt}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FormControl size="small">
|
||||
<InputLabel>Format</InputLabel>
|
||||
<Select value={format} onChange={(e) => setFormat(e.target.value)} label="Format">
|
||||
{formatOptions.map((opt) => (
|
||||
<MenuItem key={opt} value={opt}>{opt}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
{formatField && components?.FormField ? (
|
||||
<components.FormField
|
||||
name="format"
|
||||
field={formatField}
|
||||
value={format}
|
||||
onChange={setFormat}
|
||||
/>
|
||||
) : (
|
||||
<FormControl size="small">
|
||||
<InputLabel>Format</InputLabel>
|
||||
<Select value={format} onChange={(e) => setFormat(e.target.value)} label="Format">
|
||||
{formatOptions.map((opt) => (
|
||||
<MenuItem key={opt} value={opt}>{opt}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
<TextField label="From Email" value={fromEmail} onChange={(e) => setFromEmail(e.target.value)} size="small" />
|
||||
<TextField label="Subject" value={subject} onChange={(e) => setSubject(e.target.value)} size="small" />
|
||||
<TextField label="Raw Terms" value={rawTerms} onChange={(e) => setRawTerms(e.target.value)} size="small" helperText="Comma-separated search terms" />
|
||||
@@ -299,29 +314,60 @@ export default function FetchRequests() {
|
||||
)}
|
||||
sx={{ "& .MuiOutlinedInput-root": { height: "auto", minHeight: "2.5rem" } }}
|
||||
/>
|
||||
<TextField label="Payor Username" value={payorUsername} onChange={(e) => setPayorUsername(e.target.value)} size="small" helperText="Default: aetos" />
|
||||
{payorUsernameField && components?.FormField ? (
|
||||
<components.FormField
|
||||
name="payor_username"
|
||||
field={payorUsernameField}
|
||||
value={payorUsername}
|
||||
onChange={setPayorUsername}
|
||||
/>
|
||||
) : (
|
||||
<TextField label="Payor Username" value={payorUsername} onChange={(e) => setPayorUsername(e.target.value)} size="small" helperText="Default: aetos" />
|
||||
)}
|
||||
|
||||
<Box sx={{ display: "flex", gap: 2 }}>
|
||||
<TextField
|
||||
label="Start Date"
|
||||
type="date"
|
||||
value={startDate}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
size="small"
|
||||
InputLabelProps={{ shrink: true }}
|
||||
inputProps={{ max: new Date().toISOString().split("T")[0] }}
|
||||
sx={{ flex: 1 }}
|
||||
/>
|
||||
<TextField
|
||||
label="End Date"
|
||||
type="date"
|
||||
value={endDate}
|
||||
onChange={(e) => setEndDate(e.target.value)}
|
||||
size="small"
|
||||
InputLabelProps={{ shrink: true }}
|
||||
inputProps={{ max: new Date().toISOString().split("T")[0] }}
|
||||
sx={{ flex: 1 }}
|
||||
/>
|
||||
{startDateField && components?.DateField ? (
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<components.DateField
|
||||
name="start_date"
|
||||
field={startDateField}
|
||||
value={startDate}
|
||||
onChange={setStartDate}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<TextField
|
||||
label="Start Date"
|
||||
type="date"
|
||||
value={startDate}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
size="small"
|
||||
InputLabelProps={{ shrink: true }}
|
||||
inputProps={{ max: new Date().toISOString().split("T")[0] }}
|
||||
sx={{ flex: 1 }}
|
||||
/>
|
||||
)}
|
||||
{endDateField && components?.DateField ? (
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<components.DateField
|
||||
name="end_date"
|
||||
field={endDateField}
|
||||
value={endDate}
|
||||
onChange={setEndDate}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<TextField
|
||||
label="End Date"
|
||||
type="date"
|
||||
value={endDate}
|
||||
onChange={(e) => setEndDate(e.target.value)}
|
||||
size="small"
|
||||
InputLabelProps={{ shrink: true }}
|
||||
inputProps={{ max: new Date().toISOString().split("T")[0] }}
|
||||
sx={{ flex: 1 }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
@@ -385,132 +431,143 @@ export default function FetchRequests() {
|
||||
No fetch requests yet
|
||||
</Box>
|
||||
) : (
|
||||
<TableContainer component={Paper} variant="outlined" sx={{ borderRadius: 4 }}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>ID</TableCell>
|
||||
<TableCell>Account</TableCell>
|
||||
<TableCell>Source</TableCell>
|
||||
<TableCell>Date Range</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Retries</TableCell>
|
||||
<TableCell>Created</TableCell>
|
||||
<TableCell align="right">Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{[...requests]
|
||||
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
|
||||
.map((req: FetchRequest) => (
|
||||
<TableRow
|
||||
key={req.id}
|
||||
hover
|
||||
onClick={() => navigate(`/fetch-requests/${req.id}`)}
|
||||
sx={{ cursor: "pointer", "&:last-child td": { border: 0 } }}
|
||||
>
|
||||
<TableCell sx={{ fontFamily: "monospace", fontSize: "0.8rem" }}>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
|
||||
{shortId(req.fingerprint)}
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigator.clipboard.writeText(req.fingerprint);
|
||||
setSnackbar({ message: "Copied!", severity: "success" });
|
||||
}}
|
||||
sx={{ opacity: 0.5, '&:hover': { opacity: 1 } }}
|
||||
>
|
||||
<ContentCopyIcon sx={{ fontSize: 14 }} />
|
||||
</IconButton>
|
||||
<Paper variant="outlined" sx={{ borderRadius: 4 }}>
|
||||
<Box sx={{ overflowX: "auto" }}>
|
||||
<Box component="table" sx={{ width: "100%", borderCollapse: "collapse" }}>
|
||||
<Box component="thead">
|
||||
<Box component="tr" sx={{ borderBottom: 1, borderColor: "divider" }}>
|
||||
{["ID", "Account", "Source", "Date Range", "Status", "Retries", "Created", "Actions"].map((h) => (
|
||||
<Box
|
||||
key={h}
|
||||
component="th"
|
||||
sx={{ px: 2, py: 1.5, textAlign: h === "Actions" ? "right" : "left", fontWeight: 600, fontSize: "0.8rem", color: "text.secondary", whiteSpace: "nowrap" }}
|
||||
>
|
||||
{h}
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>{req.account_name}</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={"path" in req.source ? "File" : "Email"}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color={"path" in req.source ? "primary" : "secondary"}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2" sx={{ fontSize: "0.8rem", whiteSpace: "nowrap" }}>
|
||||
{formatDateRange((req as any).start_date, (req as any).end_date)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
|
||||
<Tooltip title={req.error_message || req.status.replace(/_/g, " ")}>
|
||||
<Chip
|
||||
icon={statusIcons[req.status] as any}
|
||||
label={req.status.replace(/_/g, " ")}
|
||||
color={statusColors[req.status]}
|
||||
size="small"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{(req.retry_count ?? 0) > 0 ? (
|
||||
<Typography variant="body2" sx={{ fontSize: "0.8rem" }}>
|
||||
{req.retry_count}/{RETRY_MAX}
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography variant="body2" sx={{ fontSize: "0.8rem", color: "text.disabled" }}>
|
||||
—
|
||||
</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell sx={{ whiteSpace: "nowrap", fontSize: "0.8rem" }}>
|
||||
{formatDate(req.created_at)}
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Box sx={{ display: "flex", gap: 0.5, justifyContent: "flex-end" }}>
|
||||
{req.status === "paused" && (
|
||||
<Tooltip title="Resolve ambiguities">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(`/fetch-requests/${req.id}`);
|
||||
}}
|
||||
>
|
||||
<WarningAmberIcon fontSize="small" color="warning" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{req.status === "failed" && (req.retry_count ?? 0) < RETRY_MAX && (
|
||||
<Tooltip title="Retry">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRetry(req);
|
||||
}}
|
||||
>
|
||||
<ReplayIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title="Delete">
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box component="tbody">
|
||||
{[...requests]
|
||||
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
|
||||
.map((req: FetchRequest) => (
|
||||
<Box
|
||||
key={req.id}
|
||||
component="tr"
|
||||
onClick={() => navigate(`/fetch-requests/${req.id}`)}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
borderBottom: 1,
|
||||
borderColor: "divider",
|
||||
"&:hover": { bgcolor: "action.hover" },
|
||||
"&:last-child": { borderBottom: 0 },
|
||||
}}
|
||||
>
|
||||
<Box component="td" sx={{ px: 2, py: 1.5, fontFamily: "monospace", fontSize: "0.8rem" }}>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
|
||||
{shortId(req.fingerprint)}
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setDeleteTarget(req);
|
||||
navigator.clipboard.writeText(req.fingerprint);
|
||||
setSnackbar({ message: "Copied!", severity: "success" });
|
||||
}}
|
||||
sx={{ opacity: 0.5, '&:hover': { opacity: 1 } }}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
<ContentCopyIcon sx={{ fontSize: 14 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Box component="td" sx={{ px: 2, py: 1.5, fontSize: "0.875rem" }}>
|
||||
{req.account_name}
|
||||
</Box>
|
||||
<Box component="td" sx={{ px: 2, py: 1.5 }}>
|
||||
<Chip
|
||||
label={"path" in req.source ? "File" : "Email"}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color={"path" in req.source ? "primary" : "secondary"}
|
||||
/>
|
||||
</Box>
|
||||
<Box component="td" sx={{ px: 2, py: 1.5 }}>
|
||||
<Typography variant="body2" sx={{ fontSize: "0.8rem", whiteSpace: "nowrap" }}>
|
||||
{formatDateRange((req as any).start_date, (req as any).end_date)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box component="td" sx={{ px: 2, py: 1.5 }}>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
|
||||
<Tooltip title={req.error_message || req.status.replace(/_/g, " ")}>
|
||||
<Chip
|
||||
icon={statusIcons[req.status] as any}
|
||||
label={req.status.replace(/_/g, " ")}
|
||||
color={statusColors[req.status]}
|
||||
size="small"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box component="td" sx={{ px: 2, py: 1.5 }}>
|
||||
{(req.retry_count ?? 0) > 0 ? (
|
||||
<Typography variant="body2" sx={{ fontSize: "0.8rem" }}>
|
||||
{req.retry_count}/{RETRY_MAX}
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography variant="body2" sx={{ fontSize: "0.8rem", color: "text.disabled" }}>
|
||||
\u2014
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<Box component="td" sx={{ px: 2, py: 1.5, whiteSpace: "nowrap", fontSize: "0.8rem" }}>
|
||||
{formatDate(req.created_at)}
|
||||
</Box>
|
||||
<Box component="td" sx={{ px: 2, py: 1.5 }}>
|
||||
<Box sx={{ display: "flex", gap: 0.5, justifyContent: "flex-end" }}>
|
||||
{req.status === "paused" && (
|
||||
<Tooltip title="Resolve ambiguities">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(`/fetch-requests/${req.id}`);
|
||||
}}
|
||||
>
|
||||
<WarningAmberIcon fontSize="small" color="warning" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{req.status === "failed" && (req.retry_count ?? 0) < RETRY_MAX && (
|
||||
<Tooltip title="Retry">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRetry(req);
|
||||
}}
|
||||
>
|
||||
<ReplayIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title="Delete">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setDeleteTarget(req);
|
||||
}}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
<Snackbar
|
||||
|
||||
@@ -6,12 +6,6 @@ import {
|
||||
Typography,
|
||||
TextField,
|
||||
Button,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
IconButton,
|
||||
CircularProgress,
|
||||
Alert,
|
||||
@@ -29,12 +23,22 @@ import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import AddCircleIcon from "@mui/icons-material/AddCircle";
|
||||
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
|
||||
import {
|
||||
useReportSnapshotsList,
|
||||
useCreateSnapshot,
|
||||
useDeleteSnapshot,
|
||||
} from "./features/report-snapshots";
|
||||
import type { ReportSnapshot } from "./features/report-snapshots";
|
||||
import { useResourceByName, useConfig, defaultFieldComponents } from "../react-openapi";
|
||||
import type { ResourceField } from "../react-openapi";
|
||||
|
||||
interface ReportSnapshotQuery {
|
||||
accounts?: string[];
|
||||
ignore_self?: boolean;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
}
|
||||
|
||||
interface ReportSnapshot {
|
||||
id: string;
|
||||
snapshot_id: string;
|
||||
created_at: string;
|
||||
query?: ReportSnapshotQuery;
|
||||
}
|
||||
|
||||
function formatDate(iso: string) {
|
||||
const d = new Date(iso);
|
||||
@@ -51,21 +55,32 @@ export default function ReportSnapshots() {
|
||||
const [deleteTarget, setDeleteTarget] = React.useState<ReportSnapshot | null>(null);
|
||||
const [createdSnapshotId, setCreatedSnapshotId] = React.useState<string | null>(null);
|
||||
|
||||
const { data: listData, isLoading, isFetching, refetch } = useReportSnapshotsList();
|
||||
const createMutation = useCreateSnapshot();
|
||||
const deleteMutation = useDeleteSnapshot();
|
||||
const { useList, useCreate, useDelete, components } = useResourceByName("reports", { fieldComponents: defaultFieldComponents });
|
||||
|
||||
const snapshots = listData?.data ?? [];
|
||||
const { data: listData, isLoading, isFetching, refetch } = useList();
|
||||
const createMutation = useCreate();
|
||||
const deleteMutation = useDelete();
|
||||
|
||||
const config = useConfig();
|
||||
const reportsRes = config?.resources.find((r: any) => r.name === "reports");
|
||||
const ignoreSelfField: ResourceField | undefined = reportsRes?.fields?.ignore_self;
|
||||
const startDateField: ResourceField | undefined = reportsRes?.fields?.start_date;
|
||||
const endDateField: ResourceField | undefined = reportsRes?.fields?.end_date;
|
||||
const minAmountField: ResourceField | undefined = reportsRes?.fields?.min_amount;
|
||||
const maxAmountField: ResourceField | undefined = reportsRes?.fields?.max_amount;
|
||||
|
||||
const snapshots: ReportSnapshot[] = listData?.data ?? [];
|
||||
|
||||
const handleCreate = async () => {
|
||||
try {
|
||||
const result = await createMutation.mutateAsync({
|
||||
ignore_self: ignoreSelf || null,
|
||||
start_date: startDate ? new Date(startDate).toISOString() : null,
|
||||
end_date: endDate ? new Date(endDate).toISOString() : null,
|
||||
min_amount: minAmount ? parseFloat(minAmount) : null,
|
||||
max_amount: maxAmount ? parseFloat(maxAmount) : null,
|
||||
});
|
||||
const payload: Record<string, any> = {};
|
||||
if (ignoreSelf) payload.ignore_self = true;
|
||||
if (startDate) payload.start_date = new Date(startDate).toISOString();
|
||||
if (endDate) payload.end_date = new Date(endDate).toISOString();
|
||||
if (minAmount) payload.min_amount = parseFloat(minAmount);
|
||||
if (maxAmount) payload.max_amount = parseFloat(maxAmount);
|
||||
|
||||
const result = await createMutation.mutateAsync(payload);
|
||||
const snapshotId = (result as any)?.snapshot_id;
|
||||
if (snapshotId) {
|
||||
setCreatedSnapshotId(snapshotId);
|
||||
@@ -80,7 +95,7 @@ export default function ReportSnapshots() {
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setIgnoreSelf(false);
|
||||
setIgnoreSelf(true);
|
||||
setStartDate("");
|
||||
setEndDate("");
|
||||
setMinAmount("");
|
||||
@@ -110,49 +125,102 @@ export default function ReportSnapshots() {
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||
<FormControlLabel
|
||||
control={<Switch checked={ignoreSelf} onChange={(e) => setIgnoreSelf(e.target.checked)} />}
|
||||
label="Ignore self-transfers"
|
||||
/>
|
||||
{ignoreSelfField && components?.FormField ? (
|
||||
<components.FormField
|
||||
name="ignore_self"
|
||||
field={ignoreSelfField}
|
||||
value={ignoreSelf}
|
||||
onChange={(val: boolean) => setIgnoreSelf(val)}
|
||||
/>
|
||||
) : (
|
||||
<FormControlLabel
|
||||
control={<Switch checked={ignoreSelf} onChange={(e) => setIgnoreSelf(e.target.checked)} />}
|
||||
label="Ignore self-transfers"
|
||||
/>
|
||||
)}
|
||||
|
||||
<Box sx={{ display: "flex", gap: 2 }}>
|
||||
<TextField
|
||||
label="Start Date"
|
||||
type="datetime-local"
|
||||
value={startDate}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
size="small"
|
||||
InputLabelProps={{ shrink: true }}
|
||||
sx={{ flex: 1 }}
|
||||
/>
|
||||
<TextField
|
||||
label="End Date"
|
||||
type="datetime-local"
|
||||
value={endDate}
|
||||
onChange={(e) => setEndDate(e.target.value)}
|
||||
size="small"
|
||||
InputLabelProps={{ shrink: true }}
|
||||
sx={{ flex: 1 }}
|
||||
/>
|
||||
{startDateField && components?.DateField ? (
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<components.DateField
|
||||
name="start_date"
|
||||
field={startDateField}
|
||||
value={startDate}
|
||||
onChange={(val: string) => setStartDate(val)}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<TextField
|
||||
label="Start Date"
|
||||
type="datetime-local"
|
||||
value={startDate}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
size="small"
|
||||
InputLabelProps={{ shrink: true }}
|
||||
sx={{ flex: 1 }}
|
||||
/>
|
||||
)}
|
||||
{endDateField && components?.DateField ? (
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<components.DateField
|
||||
name="end_date"
|
||||
field={endDateField}
|
||||
value={endDate}
|
||||
onChange={(val: string) => setEndDate(val)}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<TextField
|
||||
label="End Date"
|
||||
type="datetime-local"
|
||||
value={endDate}
|
||||
onChange={(e) => setEndDate(e.target.value)}
|
||||
size="small"
|
||||
InputLabelProps={{ shrink: true }}
|
||||
sx={{ flex: 1 }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: "flex", gap: 2 }}>
|
||||
<TextField
|
||||
label="Min Amount"
|
||||
type="number"
|
||||
value={minAmount}
|
||||
onChange={(e) => setMinAmount(e.target.value)}
|
||||
size="small"
|
||||
sx={{ flex: 1 }}
|
||||
/>
|
||||
<TextField
|
||||
label="Max Amount"
|
||||
type="number"
|
||||
value={maxAmount}
|
||||
onChange={(e) => setMaxAmount(e.target.value)}
|
||||
size="small"
|
||||
sx={{ flex: 1 }}
|
||||
/>
|
||||
{minAmountField && components?.FormField ? (
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<components.FormField
|
||||
name="min_amount"
|
||||
field={minAmountField}
|
||||
value={minAmount}
|
||||
onChange={(val: string) => setMinAmount(val)}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<TextField
|
||||
label="Min Amount"
|
||||
type="number"
|
||||
value={minAmount}
|
||||
onChange={(e) => setMinAmount(e.target.value)}
|
||||
size="small"
|
||||
sx={{ flex: 1 }}
|
||||
/>
|
||||
)}
|
||||
{maxAmountField && components?.FormField ? (
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<components.FormField
|
||||
name="max_amount"
|
||||
field={maxAmountField}
|
||||
value={maxAmount}
|
||||
onChange={(val: string) => setMaxAmount(val)}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<TextField
|
||||
label="Max Amount"
|
||||
type="number"
|
||||
value={maxAmount}
|
||||
onChange={(e) => setMaxAmount(e.target.value)}
|
||||
size="small"
|
||||
sx={{ flex: 1 }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
@@ -191,20 +259,29 @@ export default function ReportSnapshots() {
|
||||
No snapshots yet
|
||||
</Box>
|
||||
) : (
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Snapshot ID</TableCell>
|
||||
<TableCell>Created</TableCell>
|
||||
<TableCell>Query</TableCell>
|
||||
<TableCell align="right">Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
<Box sx={{ overflowX: "auto" }}>
|
||||
<Box component="table" sx={{ width: "100%", borderCollapse: "collapse" }}>
|
||||
<Box component="thead">
|
||||
<Box component="tr" sx={{ borderBottom: 1, borderColor: "divider" }}>
|
||||
{["Snapshot ID", "Created", "Query", "Actions"].map((h) => (
|
||||
<Box
|
||||
key={h}
|
||||
component="th"
|
||||
sx={{ px: 2, py: 1.5, textAlign: h === "Actions" ? "right" : "left", fontWeight: 600, fontSize: "0.8rem", color: "text.secondary", whiteSpace: "nowrap" }}
|
||||
>
|
||||
{h}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box component="tbody">
|
||||
{snapshots.map((snap: ReportSnapshot) => (
|
||||
<TableRow key={snap.id}>
|
||||
<TableCell sx={{ fontFamily: "monospace", fontSize: "0.8rem" }}>
|
||||
<Box
|
||||
key={snap.id}
|
||||
component="tr"
|
||||
sx={{ borderBottom: 1, borderColor: "divider", "&:last-child": { borderBottom: 0 }, "&:hover": { bgcolor: "action.hover" } }}
|
||||
>
|
||||
<Box component="td" sx={{ px: 2, py: 1.5, fontFamily: "monospace", fontSize: "0.8rem" }}>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
|
||||
{snap.snapshot_id}
|
||||
<IconButton
|
||||
@@ -218,9 +295,11 @@ export default function ReportSnapshots() {
|
||||
<ContentCopyIcon sx={{ fontSize: 14 }} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>{formatDate(snap.created_at)}</TableCell>
|
||||
<TableCell>
|
||||
</Box>
|
||||
<Box component="td" sx={{ px: 2, py: 1.5, fontSize: "0.875rem" }}>
|
||||
{formatDate(snap.created_at)}
|
||||
</Box>
|
||||
<Box component="td" sx={{ px: 2, py: 1.5 }}>
|
||||
{snap.query ? (
|
||||
<Box sx={{ display: "flex", gap: 0.5, flexWrap: "wrap" }}>
|
||||
{snap.query.accounts && <Chip label={`${snap.query.accounts.length} account(s)`} size="small" variant="outlined" />}
|
||||
@@ -229,19 +308,21 @@ export default function ReportSnapshots() {
|
||||
{snap.query.end_date && <Chip label="end" size="small" variant="outlined" />}
|
||||
</Box>
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary">—</Typography>
|
||||
<Typography variant="body2" color="text.secondary">\u2014</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton size="small" onClick={() => setDeleteTarget(snap)}>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</Box>
|
||||
<Box component="td" sx={{ px: 2, py: 1.5 }}>
|
||||
<Box sx={{ display: "flex", gap: 0.5, justifyContent: "flex-end" }}>
|
||||
<IconButton size="small" onClick={() => setDeleteTarget(snap)}>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
|
||||
@@ -17,11 +17,6 @@ export type {
|
||||
} from "./fetch-requests.models";
|
||||
export { RETRY_MAX, formatApiError } from "./fetch-requests.models";
|
||||
export {
|
||||
useFetchRequestsList,
|
||||
useFetchRequest,
|
||||
useCreateFetchRequest,
|
||||
useUpdateFetchRequest,
|
||||
useDeleteFetchRequest,
|
||||
useUploadFile,
|
||||
useFetchRequestAmbiguities,
|
||||
useResolveAmbiguity,
|
||||
|
||||
@@ -2,8 +2,3 @@ export type {
|
||||
ReportSnapshot,
|
||||
ReportQuery,
|
||||
} from "./report-snapshots.models";
|
||||
export {
|
||||
useReportSnapshotsList,
|
||||
useCreateSnapshot,
|
||||
useDeleteSnapshot,
|
||||
} from "./useReportSnapshots";
|
||||
|
||||
@@ -15,7 +15,7 @@ import Dashboard from './Dashboard';
|
||||
import FetchRequests from './FetchRequests';
|
||||
import FetchRequestDetail from './FetchRequestDetail';
|
||||
import ReportSnapshots from './ReportSnapshots';
|
||||
import { Admin, AppProvider } from '../react-openapi';
|
||||
import { Admin, AppProvider, defaultFieldComponents } from '../react-openapi';
|
||||
import { configuration, profileConfiguration } from './openapi-config';
|
||||
import { Buffer } from 'buffer';
|
||||
import process from 'process';
|
||||
@@ -60,7 +60,7 @@ root.render(
|
||||
path={path}
|
||||
element={
|
||||
path.startsWith("/admin") ? (
|
||||
<Component basePath="/admin" />
|
||||
<Component basePath="/admin" fieldComponents={{ ...defaultFieldComponents }} />
|
||||
) : (
|
||||
<Component />
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user