marmelab / react-admin

A frontend Framework for single-page applications on top of REST/GraphQL APIs, using TypeScript, React and Material Design
http://marmelab.com/react-admin
MIT License
24.8k stars 5.23k forks source link

Adding fetched resources returns child.props is undefined on CoreAdminRouter #3728

Closed lsaul closed 4 years ago

lsaul commented 5 years ago

I'm attempting to add resources based on an API call. I can add knownResources without issues, I can also add fetchedResources on their own - however I cannot combine the two.

The docs show how you can display resources based on permissions queried from the API, but it doesn't detail how to add resources dynamically if they are not declared:

https://marmelab.com/react-admin/Admin.html#declaring-resources-at-runtime

If the two are combined (as below) the CoreAdminRouter throws an error:
TypeError: child.props is undefined

Any ideas on how to resolve are appreciated! The code is below:

import 'babel-polyfill';
import React from 'react';
import { Admin, Resource } from 'react-admin';
import { restClient } from 'ra-data-feathers';
import { Route } from 'react-router-dom'; 
import feathersClient from './feathersClient'; 
import englishMessages from 'ra-language-english';

// import customRoutes from './customRoutes';
import { createBrowserHistory as createHistory } from 'history';

import createRealtimeSaga from "./createRealtimeSaga";

import { Contacts } from './services/contacts';
import { Group, GroupMember } from './services/groups';
import { Albums, Photos } from './services/photos';

import UserIcon from '@material-ui/icons/AccountCircle';
import GroupIcon from '@material-ui/icons/Group';
import GroupIcon from '@material-ui/icons/GroupWork';
import StarIcon from '@material-ui/icons/StarRate';
import FolderSpecialIcon from '@material-ui/icons/FolderSpecial';

const authClientOptions = {
    storageKey: 'feathers-jwt',
    authenticate: { strategy: 'local' }
};

const history = createHistory();

const options = {
    usePatch: false, // Use PATCH instead of PUT for UPDATE requests. Optional.
    contacts: { // Options for individual resources can be set by adding an object with the same name. Optional.
        id: 'ContactId' // If this specific table uses an id field other than 'id'. Optional.
    },
    group: { // Options for individual resources can be set by adding an object with the same name. Optional.
        id: 'GroupId' // If this specific table uses an id field other than 'id'. Optional.
    },
    groupmember: { // Options for individual resources can be set by adding an object with the same name. Optional.
        id: 'GroupMemberID' // If this specific table uses an id field other than 'id'. Optional.
    },
    albums: { // Options for individual resources can be set by adding an object with the same name. Optional.
        id: 'AlbumId' // If this specific table uses an id field other than 'id'. Optional.
    },
    photos: { // Options for individual resources can be set by adding an object with the same name. Optional.
        id: 'PhotoId' // If this specific table uses an id field other than 'id'. Optional.
    }, 
    "photos/albums/1/": { // Options for individual resources can be set by adding an object with the same name. Optional.
        id: 'AlbumId' // If this specific table uses an id field other than 'id'. Optional.
    },
    "photos/albums/2/": { // Options for individual resources can be set by adding an object with the same name. Optional.
        id: 'AlbumId' // If this specific table uses an id field other than 'id'. Optional.
    },

}

const dataProvider = restClient(feathersClient, options)

const realTimeSaga = createRealtimeSaga(dataProvider);

const messages = {
    en: englishMessages,
}
const i18nProvider = locale => messages[locale];

const knownResources = [

    <Resource name="contacts" list={ContactsList} show={ContactsShow}  icon={UserIcon} />,
    <Resource name="group"  list={GroupList} show={GroupShow} icon={GroupIcon}/>,
    <Resource name="groupmember"  list={GroupMemberList} show={GroupMemberShow} icon={GroupIcon}/>,
    <Resource name="albums"  list={AlbumList} show={AlbumsShow} icon={GroupIcon}/>,
    <Resource name="photos"  list={PhotoList} show={PhotosShow} icon={GroupIcon}/>,

    //******************************************************************
    //**These resources need to be dynamically Added based on API Call**
    //******************************************************************
    // <Resource name="photos/albums/1/" list={PublicPhotosList} icon={StarIcon}/>,
    // <Resource name="photos/albums/2/" list={PublicPhotosList} icon={StarIcon}/>,
    // <Resource name="photos/albums/3/" list={PublicPhotosList} icon={StarIcon}/>,
    // etc.

];

// const fetchResources = permissions =>
//     fetch('https://myapi/resources', {
//         method: 'POST',
//         headers: {
//             'Content-Type': 'application/json'
//         },
//         body: JSON.stringify(permissions),
//     })
//     .then(response => response.json())
//     .then(json => knownResources.filter(resource => json.resources.includes(resource.props.name)));

const fetchResources = () => 
    fetch('https://jsonplaceholder.typicode.com/albums/hasphotos/true?$limit=10')
    .then(function(response){return response.json()})
    .catch(error => console.error('Error:', error))
    .then(function(schemas){
        var filtered = schemas.data
        // console.log(schema.data)
        return filtered.map((schema, index)=>{
            let name = 'photos/album/'+schema.AlbumId

            options[name] = {
                    id: 'AlbumId'
            }

            var resource;

            if(schema.Results.length!==0){

                resource = <Resource  
                name={'photos/albums/'+schema.AlbumId}
                list={PublicPhotosList}
                options = {{
                    label:schema.AlbumName
                }}/>;

                knownResources.push(resource);
                console.log(knownResources)
            }
            return knownResources
        })
    })
    .catch(error => console.error('Error:', error))

const App = () => (
    <Admin 
        // authProvider={authClient(feathersClient, authClientOptions)}
        restClient={restClient(feathersClient, options)}
        dataProvider={restClient(feathersClient, options)}
        customSagas={[realTimeSaga]}
        locale="en"
        i18nProvider={i18nProvider}
        // customRoutes={customRoutes}
        history={history}
        // theme={theme} 
    >
         //{knownResources}
        {fetchResources} 
    </Admin>

);

export default App;

If you consider this a support request, here is the question on stack overflow: https://stackoverflow.com/questions/58065717/adding-fetched-resources-to-knownresources-based-on-api-call

djhi commented 4 years ago

Please follow the issue template

lsaul commented 4 years ago

What you were expecting: The expected behavior is to create 'Resource' items based on an API call, and dynamically add them to the known Resource array

What happened instead: Admin fails to load and returns TypeError: child.props is undefined on the CoreAdminRouter react element

Steps to reproduce: Please see below code example

Environment react-admin version: 2.9.6 react-dom version: 16.8.6 browser: Firefox Error: TypeError: child.props is undefined

Any ideas on how to resolve are appreciated! The code is below:

   import 'babel-polyfill';
    import React from 'react';
    import { Admin, Resource } from 'react-admin';
    import { restClient } from 'ra-data-feathers';
    import { Route } from 'react-router-dom'; 
    import feathersClient from './feathersClient'; 
    import englishMessages from 'ra-language-english';

    // import customRoutes from './customRoutes';
    import { createBrowserHistory as createHistory } from 'history';

    import createRealtimeSaga from "./createRealtimeSaga";

    import { Contacts } from './services/contacts';
    import { Group, GroupMember } from './services/groups';
    import { Albums, Photos } from './services/photos';

    import UserIcon from '@material-ui/icons/AccountCircle';
    import GroupIcon from '@material-ui/icons/Group';
    import GroupIcon from '@material-ui/icons/GroupWork';
    import StarIcon from '@material-ui/icons/StarRate';
    import FolderSpecialIcon from '@material-ui/icons/FolderSpecial';

    const authClientOptions = {
        storageKey: 'feathers-jwt',
        authenticate: { strategy: 'local' }
    };

    const history = createHistory();

    const options = {
        usePatch: false, // Use PATCH instead of PUT for UPDATE requests. Optional.
        contacts: { // Options for individual resources can be set by adding an object with the same name. Optional.
            id: 'ContactId' // If this specific table uses an id field other than 'id'. Optional.
        },
        group: { // Options for individual resources can be set by adding an object with the same name. Optional.
            id: 'GroupId' // If this specific table uses an id field other than 'id'. Optional.
        },
        groupmember: { // Options for individual resources can be set by adding an object with the same name. Optional.
            id: 'GroupMemberID' // If this specific table uses an id field other than 'id'. Optional.
        },
        albums: { // Options for individual resources can be set by adding an object with the same name. Optional.
            id: 'AlbumId' // If this specific table uses an id field other than 'id'. Optional.
        },
        photos: { // Options for individual resources can be set by adding an object with the same name. Optional.
            id: 'PhotoId' // If this specific table uses an id field other than 'id'. Optional.
        }, 
        "photos/albums/1/": { // Options for individual resources can be set by adding an object with the same name. Optional.
            id: 'AlbumId' // If this specific table uses an id field other than 'id'. Optional.
        },
        "photos/albums/2/": { // Options for individual resources can be set by adding an object with the same name. Optional.
            id: 'AlbumId' // If this specific table uses an id field other than 'id'. Optional.
        },

    }

    const dataProvider = restClient(feathersClient, options)

    const realTimeSaga = createRealtimeSaga(dataProvider);

    const messages = {
        en: englishMessages,
    }
    const i18nProvider = locale => messages[locale];

    const knownResources = [

        <Resource name="contacts" list={ContactsList} show={ContactsShow}  icon={UserIcon} />,
        <Resource name="group"  list={GroupList} show={GroupShow} icon={GroupIcon}/>,
        <Resource name="groupmember"  list={GroupMemberList} show={GroupMemberShow} icon={GroupIcon}/>,
        <Resource name="albums"  list={AlbumList} show={AlbumsShow} icon={GroupIcon}/>,
        <Resource name="photos"  list={PhotoList} show={PhotosShow} icon={GroupIcon}/>,

        //******************************************************************
        //**These resources need to be dynamically Added based on API Call**
        //******************************************************************
        // <Resource name="photos/albums/1/" list={PublicPhotosList} icon={StarIcon}/>,
        // <Resource name="photos/albums/2/" list={PublicPhotosList} icon={StarIcon}/>,
        // <Resource name="photos/albums/3/" list={PublicPhotosList} icon={StarIcon}/>,
        // etc.

    ];

    // const fetchResources = permissions =>
    //     fetch('https://myapi/resources', {
    //         method: 'POST',
    //         headers: {
    //             'Content-Type': 'application/json'
    //         },
    //         body: JSON.stringify(permissions),
    //     })
    //     .then(response => response.json())
    //     .then(json => knownResources.filter(resource => json.resources.includes(resource.props.name)));

    const fetchResources = () => 
        fetch('https://jsonplaceholder.typicode.com/albums/hasphotos/true?$limit=10')
        .then(function(response){return response.json()})
        .catch(error => console.error('Error:', error))
        .then(function(schemas){
            var filtered = schemas.data
            // console.log(schema.data)
            return filtered.map((schema, index)=>{
                let name = 'photos/album/'+schema.AlbumId

                options[name] = {
                        id: 'AlbumId'
                }

                var resource;

               //Checks if the album has photos
               //if so, create the resource
                if(schema.albums.length!==0){

                    resource = <Resource  
                    name={'photos/albums/'+schema.AlbumId}
                    list={PublicPhotosList}
                    options = {{
                        label:schema.AlbumName
                    }}/>;

                    knownResources.push(resource);
                    console.log(knownResources)
                }
                return knownResources
            })
        })
        .catch(error => console.error('Error:', error))

    const App = () => (
        <Admin 
            // authProvider={authClient(feathersClient, authClientOptions)}
            restClient={restClient(feathersClient, options)}
            dataProvider={restClient(feathersClient, options)}
            customSagas={[realTimeSaga]}
            locale="en"
            i18nProvider={i18nProvider}
            // customRoutes={customRoutes}
            history={history}
            // theme={theme} 
        >
             //{knownResources}
            {fetchResources} 
        </Admin>

    );

    export default App;
fzaninotto commented 4 years ago

Hi, and thanks for your question. As explained in the react-admin contributing guide, the right place to ask a "How To" question, get usage advice, or troubleshoot your own code, is StackOverFlow.

This makes your question easy to find by the core team, and the developer community. Unlike Github, StackOverFlow has great SEO, gamification, voting, and reputation. That's why we chose it, and decided to keep GitHub issues only for bugs and feature requests.

So I'm closing this issue, and inviting you to ask your question at:

http://stackoverflow.com/questions/tagged/react-admin

And once you get a response, please continue to hang out on the react-admin channel in StackOverflow. That way, you can help newcomers and share your expertise!

lsaul commented 4 years ago

Hi fzaninotto, thanks for responding. I posted here because I was hoping to get the attention of the core team, since I haven't heard back stack overflow.

https://stackoverflow.com/questions/58065717/adding-fetched-resources-to-knownresources-based-on-api-call

I have to guess that the use case I described is fairly common. If it's not possible to dynamically declare Resources based on an API call, then that would seem to be a significant restriction.

In any case I'll keep working on a solution and post if I figure it out. Thanks.