I'm using react-admin v2.3.2 with a custom dashboard component as shown in the react-admin tutorial.
<Admin dashboard={MyDashboard}>
<Resource name="incidents ... />
</Admin>
Now I'd like to display a list of incidents on my dashboard using the component of react-admin but react-admin complains about missing properties like 'hasEdit'.
I simply passed the props of the dashboard component to the List but this does obviously not work:
class MyDashboard extends React.Component {
constructor(props) {
super(props)
render(
return <List {...this.props}>
<Datagrid> .... </Datagrid>
</List>
)
}
Is it possible to use react-admin's <List /> component on the dashboard and if so how can this be done?
Thanks in advance,
Thomas
I finally managed to use react-admin's components by faking the required props. Within the MyDashboard component I define props required by the component:
let fakeProps = {
basePath: "/incidents",
hasCreate: false,
hasEdit: false,
hasList: true,
hasShow: false,
history: {},
location: { pathname: "/", search: "", hash: "", state: undefined },
match: { path: "/", url: "/", isExact: true, params: {} },
options: {},
permissions: null,
resource: "incidents"
}
<List {...fakeProps}>
<DataGrid>
<TextField .... />
</DataGrid>
</List>
This is indeed a sub-optimal solution but in the first run it solves my problem.
We had a request to create a List in the Dashboard so I had use the accepted answer. Though pagination wouldn't trigger new requests to the server even though the url was changing.
This is the final solution that works with pagination using react-router v4.
In <Admin dashboard={Dashboard}> I added:
<Resource name="dashboard-stats"/>
In the Dashboard.js this is what I have:
import React, { Component } from 'react';
import { GET_LIST } from 'react-admin';
import Card from '#material-ui/core/Card';
import CardHeader from '#material-ui/core/CardHeader';
import dataProvider from './dataProvider';
import {
List,
TextField,
Datagrid
} from 'react-admin';
export default class Dashboard extends Component {
state = {};
initProps = {
basePath: "/",
hasCreate: false,
hasEdit: false,
hasList: true,
hasShow: false,
location: { pathname: "/", search: "", hash: "", state: undefined },
match: { path: "/", url: "/", isExact: true, params: {} },
options: {
},
permissions: null,
resource: "dashboard-stats",
perPage: 5
};
componentWillMount() {
this.unlisten = this.props.history.listen((location, action) => {
if (location.search) {
//regex from: https://stackoverflow.com/a/8649003/1501205
let queryParams = JSON.parse('{"' + decodeURI(location.search).replace(/"/g, '\\"').replace(/&/g, '","').replace(/=/g,'":"') + '"}')
let perPage = queryParams.perPage;
let page = queryParams.page;
this.initProps.perPage = perPage;
this.initProps.page = page;
this.initProps.location = location;
this.setState({'initProps': this.initProps})
}
});
}
componentWillUnmount() {
this.unlisten();
}
componentDidMount() {
this.setState({'initProps': this.initProps});
dataProvider(GET_LIST, 'stats', {
sort: { field: 'date', order: 'DESC' },
pagination: { page: 1, perPage: 50 },
})
.then(response => {
this.setState({'stats': response.data});
});
}
render() {
const {
initProps
} = this.state;
if(!initProps) {
return false;
}
return <Card>
<CardHeader title="Welcome to the Dashboard" />
<List {...initProps} >
<Datagrid>
<TextField source="description" />
<TextField source="state" />
<TextField source="date" />
</Datagrid>
</List>
</Card>;
}
}
don't forget to update the location this.initProps.location with the location just changed - otherwise it will work for the first click (route change) and then it will stop working
I came across this page researching similar.
The way to do this now (late 2020) is to create a ListContextProvider on the page for the main resource you want to show on the dashboard so you get everything provided by that context, including filters.
const controllerProps = useListController(props);
<ListContextProvider value={controllerProps}>
<MyDashboardView>
</ListContextProvider>
You must be wanting to display data from various resources, otherwise you would just use a regular "List" page.
Dashboard can do this. Have a look at the Demo Dashboard
Multiple dataProvider(GET_LIST,... that you pass on to components. You can use this Demo Dashboard Component as an example. Pending Orders
Thanks for the solution. Screen renders with below warning
How to remove all warnings ?
Warning: React does not recognize the basePath prop on a DOM element. If you intentionally want it to appear in the DOM as a custom attribute, spell it as lowercase basepath instead. If you accidentally passed it from a parent component, remove it from the DOM element.
Like basePath console shows same warnings with other props "currentSort, defaultTitle, displayedFilters,filterValues,hasCreate,hideFilter,isLoading,loadedOnce,onToggleItem,onUnselectItems,perPage,selectedIds,setFilters,setPage,setPerPage,setSort,showFilter,hasBulkActions"
Warning: Invalid value for prop translate on tag. Either remove it from the element, or pass a string or number value to keep it in the DOM. For details
const initProps = {
basePath: '/',
hasCreate: false,
hasEdit: false,
hasList: true,
hasShow: false,
location: { pathname: '/', search: '', hash: '', state: undefined },
match: { path: '/', url: '/', isExact: true, params: {} },
options: {},
permissions: null,
resource: 'dashboard',
perPage: 5
};
<List
{...initProps}
filterDefaultValues={{
arrivedTimestampStart: moment().format('YYYY-MM-DD'),
arrivedTimestamp: moment().format('YYYY-MM-DD')
}}
filters={<DashboardFilter />}
sort={{ field: 'arrivedTimestamp', order: 'DESC' }}
pagination={<Fragment />}
exporter={false}
>
<Responsive
medium={
<div style={styles.flex}>
<OtherComponent />
</div>
}
/>
</List>
I think the answer in v4+ is now the useList hook, which you can see on the List docs page: https://marmelab.com/react-admin/List.html. Copied from that page:
const data = [
{ id: 1, name: 'Arnold' },
{ id: 2, name: 'Sylvester' },
{ id: 3, name: 'Jean-Claude' },
]
const ids = [1, 2, 3];
const MyComponent = () => {
const listContext = useList({
data,
ids,
basePath: '/resource',
resource: 'resource',
});
return (
<ListContextProvider value={listContext}>
<Datagrid>
<TextField source="id" />
<TextField source="name" />
</Datagrid>
</ListContextProvider>
);
};
Related
I am using dynamic links for deep linking.
const linking = {
prefixes: [
www.example.com
],
config: {
screens: {
Chat: {
path: ":id",
parse: {
id: (id) => `${id}`,
},
},
Profile: 'user',
},
},
};
function App() {
return (
<NavigationContainer linking={linking} fallback={<Text>Loading...</Text>}>
<Stack.Screen name="Chat" component={ChatScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
</NavigationContainer>
);
}
www.example.com/user always route to the ChatScreen screen. I want to route in ProfileScreen. How to handle config file in linking?
Set your config as
const config = {
screens: {
Profile: '/user',
Chat:'/chat/:id'
},
};
your linking should be
const linking = {
prefixes: [
'www.example.com',
],
config,
};
By doing so, www.example.com/user will be routed to ProfileScreen component. www.example.com/chat/:<some id> will take you to ChatScreen with route param as id.
Update: It seems you are trying to go to ChatScreen by www.example.com/<some id> and also have www.example.com/user to load ProfileScreen. The problem is React navigation considers string "user" as param id and takes you to ChatScreen itself. That is the reason I added "chat" in the path of ChatScreen.
If you really want to use www.example.com/<some id> to load ChatScreen, you can use getStateFromPath and write your own logic to differentiate between ids and path names. To do that, your linking will be
const linking = {
prefixes: [
'www.example.com',
],
config,
getStateFromPath: (path, options) => {
if (path ==='/user') {
return {routes: [{ name: 'Profile' }],
} ;
} else {
return {routes: [{ name: 'Chat',params:{id:getIdFromPath(path)} }],
} ;
}
},
};
here checkIfValidPath is for checking whether the url path is an actual id or "user". getIdFromPath is to extract id from the url. I havent tested this, but this should work.
const checkIfValidPath=(path)=>{
return path !=='www.example.com/user';
}
const getIdFromPath =(path)=>{
return path.slice(1, path.length);
}
This worked for me
const getIdFromPath = (path) => { return path.slice(1, path.length); }
const linking = {
prefixes: [
www.example.com
],
config: {
screens: {
Chat: {
path: ":id",
parse: {
id: (id) => `${id}`,
},
},
Profile: 'user',
},
},
getStateFromPath: (path, options) => {
if (path === '/user') {
return { routes: [{ name: 'Profile' }], };
}
else {
return { routes: [{ name: 'Chat', params: { id: getIdFromPath(path) } }], };
}
}
};
function App() {
return (
<NavigationContainer linking={linking} fallback={<Text>Loading...</Text>}>
<Stack.Screen name="Chat" component={ChatScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
</NavigationContainer>
);
}
I want add an import button for my react admin application.
This import will create records related to another record. For instance, I have a list of contracts that belong to a company.
I saw this repo: https://github.com/benwinding/react-admin-import-csv
And I was wondering if there was something similar for a referenceManyField.
I found a hacky solution for this in case anyone is interested.
Instead of using a ReferenceManyField I used a List inside a Show component.
To be able to do this I had to override the props of the list to specify the resource that I wanted to reference.
I also had to pass the show item id as a prop of the list component and filter the list by the id.
let companyId = '';
const ListActions = (props) => {
const { className } = props;
return (
<TopToolbar className={className}>
<ImportButton {...props} />
</TopToolbar>
);
};
export const ContractRow = ({ contractCompanyId }) => {
companyId = contractCompanyId;
const fakeProps: ListProps = {
basePath: '/contracts',
hasCreate: false,
hasEdit: false,
hasList: true,
hasShow: false,
location: { pathname: '/', search: '', hash: '', state: undefined },
match: { path: '/', url: '/', isExact: true, params: {} },
options: {},
permissions: null,
resource: 'contracts',
};
return (
<List
{...fakeProps}
filter={{ _company_id: contractCompanyId }}
actions={<ListActions />}
>
<Datagrid className={classes.insideTable}>
<TextField source="name" />
</Datagrid>
</List>
);
};
I am using react material-table. I am in need of a feature like this: I use remote data mode to get a list of records, then I use the custom column rendering function to add a Material Button at the end of each row in the table, When the user presses this button I want it to be hidden. How can I do that. I look forward to receiving your help.
This is the illustration image
I made this example, on button click it gets disabled and a variable is set to to a loading state:
The key aspect here is to define something that identifies the row that is being updated. I use an extra column on which you could also display a spinner component:
{
field: "isUpdating",
render: (rowdata) =>
fetchingClient === rowdata.name
? "loading.." // Add your <Spinner />
: null
},
Since you want to render the button as a custom column (other way could be using actions), on the render attribute of that column, you can use rowdata parameter to access what you are looking for:
{
field: "join",
sorting: false,
render: (rowdata) => (
<button
disabled={fetchingClient === rowdata.name}
onClick={(event) => fetchDataFromRemote(rowdata.name)}
>
Go fetch
</button>
)
}
Here is the link to the sandbox and the complete code, I hope this works for you!
import React, { Fragment, useState } from "react";
import MaterialTable from "material-table";
export default function CustomEditComponent(props) {
const [fetchingClient, setFetchingClient] = useState("");
const fetchDataFromRemote = (clientName) => {
console.log(clientName);
setFetchingClient(clientName);
};
const tableColumns = [
{ title: "Client", field: "client" },
{ title: "Name", field: "name" },
{
field: "isUpdating",
render: (rowdata) =>
fetchingClient === rowdata.name
? "loading.." // Add your <Spinner />
: null,
},
{
field: "join",
sorting: false,
render: (rowdata) => (
<button
disabled={fetchingClient === rowdata.name}
onClick={(event) => fetchDataFromRemote(rowdata.name)}
>
Go fetch
</button>
),
},
];
const tableData = [
{
client: "client1",
name: "Jasnah",
year: "2019",
},
{
client: "client2",
name: "Dalinar",
year: "2018",
},
{
client: "client3",
name: "Kal",
year: "2019",
},
];
return (
<Fragment>
<MaterialTable
columns={tableColumns}
data={tableData}
title="Material Table - custom column "
options={{ search: false }}
/>
</Fragment>
);
}
Receive below errors, when using Datagrid component with custom queries. Below code works with react-admin ver 3.3.1, whereas it doesn't work with ver 3.8.1
TypeError: Cannot read property 'includes' of undefined
Browser's console info: List components must be used inside a <ListContext.Provider>. Relying on props rather than context to get List data and callbacks is deprecated and won't be supported in the next major version of react-admin.
Refer: https://marmelab.com/react-admin/List.html
#Tip: You can use the Datagrid component with custom queries:
import keyBy from 'lodash/keyBy'
import { useQuery, Datagrid, TextField, Pagination, Loading } from 'react-admin'
const CustomList = () => {
const [page, setPage] = useState(1);
const perPage = 50;
const { data, total, loading, error } = useQuery({
type: 'GET_LIST',
resource: 'posts',
payload: {
pagination: { page, perPage },
sort: { field: 'id', order: 'ASC' },
filter: {},
}
});
if (loading) {
return <Loading />
}
if (error) {
return <p>ERROR: {error}</p>
}
return (
<>
<Datagrid
data={keyBy(data, 'id')}
ids={data.map(({ id }) => id)}
currentSort={{ field: 'id', order: 'ASC' }}
basePath="/posts" // required only if you set use "rowClick"
rowClick="edit"
>
<TextField source="id" />
<TextField source="name" />
</Datagrid>
<Pagination
page={page}
perPage={perPage}
setPage={setPage}
total={total}
/>
</>
)
} ```
Please help!
Since react-admin 3.7, <Datagrid> and <Pagination> read data from a ListContext, instead of expecting the data to be injected by props. See for instance the updated <Datagrid> docs at https://marmelab.com/react-admin/List.html#the-datagrid-component.
Your code will work if you wrap it in a <ListContextProvider>:
import React, { useState } from 'react';
import keyBy from 'lodash/keyBy'
import { useQuery, Datagrid, TextField, Pagination, Loading, ListContextProvider } from 'react-admin'
export const CustomList = () => {
const [page, setPage] = useState(1);
const perPage = 50;
const { data, total, loading, error } = useQuery({
type: 'GET_LIST',
resource: 'posts',
payload: {
pagination: { page, perPage },
sort: { field: 'id', order: 'ASC' },
filter: {},
}
});
if (loading) {
return <Loading />
}
if (error) {
return <p>ERROR: {error}</p>
}
return (
<ListContextProvider value={{
data: keyBy(data, 'id'),
ids: data.map(({ id }) => id),
total,
page,
perPage,
setPage,
currentSort: { field: 'id', order: 'ASC' },
basePath: "/posts",
resource: 'posts',
selectedIds: []
}}>
<Datagrid rowClick="edit">
<TextField source="id" />
<TextField source="name" />
</Datagrid>
<Pagination />
</ListContextProvider >
)
}
<ReferenceManyField>, as well as other relationship-related components, also implement a ListContext. That means you can use a <Datagrid> of a <Pagination> inside this component.
https://marmelab.com/react-admin/List.html#uselistcontext
Your code should look like this:
import React, { useState } from 'react';
import keyBy from 'lodash/keyBy'
import { useQuery, Datagrid, TextField, Pagination, Loading, ListContextProvider } from 'react-admin'
export const CustomList = () => {
return (
<ReferenceManyField reference="Your resource for pull the data" target="linked field">
<Datagrid rowClick="edit">
<TextField source="id" />
<TextField source="name" />
</Datagrid>
</ReferenceManyField>
)
}
I'm trying to upgrade a custom app from admin-on-rest to react-admin (v2.15). After I figured out that the declareResources action was "replaced" by registerResource most seemed ok, but I still struggle with the List and Edit components route definitions that complains about missing required props (compared to what props are defined in the custom app documentation).
If I define a List component like this it works fine:
<Route exact path="/mystuffs" render={(routeProps) => <MystuffList hasCreate hasEdit hasShow={false} hasList resource="mystuffs" basePath="/mystuffs" {...routeProps} />} />
Similar the only way I can get an Edit-component to work is to pass the required props like so:
<Route exact path="/mystuffs/:id" render={(routeProps) => <MystuffEdit resource="mystuffs" id={routeProps.match.params.id} basePath="/mystuffs" {...routeProps} />} />
But to me it seems a bit tedious to define all of these props (i.e was not required with admin-on-rest). Is this the correct way of doing it or am I missing something obvious here since the custom app documentation doesn't specify all of the required props?
I ended up adding a custom resource component that is pretty similar to the Resource in react-admin. Something like this:
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { Switch, Route } from 'react-router-dom';
const RACustomResource = ({ resource, list, edit, create, show, path, children, ...rest }) => {
const { computedMatch, ...options } = rest;
const listResource = list ?
(<Route
exact
path={path}
render={(routeProps) => {
const { staticContext, ...routeOpts } = routeProps;
return React.createElement(list, {
basePath: path || routeProps.match.url,
resource,
hasCreate: !!create,
hasList: !!list,
hasEdit: !!edit,
hasShow: !!show,
...routeOpts,
...options,
});
}}
/>)
: null;
const createResource = create ?
(<Route
path={`${path}/create`}
render={(routeProps) => {
const { staticContext, ...routeOpts } = routeProps;
return React.createElement(create, {
basePath: path || routeProps.match.url,
resource,
hasList: !!list,
hasShow: !!show,
record: {},
...routeOpts,
...options,
});
}}
/>)
: null;
const editResource = edit ?
(<Route
exact
path={`${path}/:id`}
render={(routeProps) => {
const { staticContext, ...routeOpts } = routeProps;
return React.createElement(edit, {
basePath: path || routeProps.match.url,
resource,
hasCreate: !!create,
hasList: !!list,
hasEdit: !!edit,
hasShow: !!show,
id: routeProps.match.params.id,
...routeOpts,
...options,
});
}}
/>)
: null;
const showResource = show ?
(<Route
exact
path={`${path}/:id/show`}
render={(routeProps) => {
const { staticContext, ...routeOpts } = routeProps;
return React.createElement(show, {
basePath: path || routeProps.match.url,
resource,
hasCreate: !!create,
hasList: !!list,
hasEdit: !!edit,
hasShow: !!show,
id: routeProps.match.params.id,
...routeOpts,
...options,
});
}}
/>)
: null;
return (
<Switch>
{createResource}
{showResource}
{editResource}
{listResource}
{children}
</Switch>
);
};
RACustomResource.propTypes = {
resource: PropTypes.string.isRequired,
path: PropTypes.string,
basePath: PropTypes.string,
children: PropTypes.any,
list: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
create: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
edit: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
show: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
};
export default RACustomResource;
// used like this
// <RACustomResource path="/myresource" resource="myresource" list={MyResourceList} create={MyResourceCreate} edit={MyResourceEdit} />
It is indeed required. We have still a lot of work to do on the custom app side, including documentation.
You can help us! Can you explain why you needed to use react-admin this way? What wasn't possible using the default Admin? etc.
Thanks!