Hello :star2: I have an issue regarding the works...
# gooddata-ui
n
Hello 🌟 I have an issue regarding the workspaces. In my GoodData app i can only log in with the user that has the workspace ID that matches the workspace ID that i have in my constants file. If i log in with a user that has another workspace id - that does not match the one in my constants file i get a blank page at first. After I update the page then does my workspace picker set the workspace of the current loged in user. I Used the gooddata package to start my project (the plugin that already provides the basic files) this is my Workspace.tsx:
import React, { createContext, useState, useContext, useEffect } from "react";
import { useQueryState } from "react-router-use-location-state";
import { WorkspaceProvider as DefaultWorkspaceProvider } from "@gooddata/sdk-ui";
import identity from "lodash/identity";
import { workspace as defaultWorkspace } from "../constants";
import { useWorkspaceList } from "./WorkspaceList";
interface IWorkspaceContext {
workspace: string;
setWorkspace: (workspace: string) => void;
}
export const WorkspaceContext = createContext<IWorkspaceContext>({
workspace: defaultWorkspace,
setWorkspace: identity,
});
export const WorkspaceProvider: React.FC = ({ children }) => {
const workspaceList = useWorkspaceList();
const [queryWorkspace, setQueryWorkspace] = useQueryState<string>("workspace", defaultWorkspace);
const [workspace, setWorkspace] = useState<string>(queryWorkspace);
// update query string with actual workspace
useEffect(() => {
setQueryWorkspace(workspace);
//console.log("TAG query workspace: ", workspace);
// Do not include setQueryWorkspace into effect dependencies
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [workspace, queryWorkspace]);
// if workspace was not set yet then try to use first workspace available
useEffect(() => {
if (!workspace && workspaceList.firstWorkspace) {
setWorkspace(workspaceList.firstWorkspace);
//console.log("TAG first !!!workspace: ", workspaceList.firstWorkspace);
}
if (workspaceList.firstWorkspace) {
//console.log("TAG workspaceList.firstWorkspace: ", workspaceList.firstWorkspace);
setWorkspace(workspaceList.firstWorkspace);
}
}, [workspace, workspaceList]);
return (
<WorkspaceContext.Provider value={{ workspace, setWorkspace }}>
<DefaultWorkspaceProvider workspace={workspace}>{children}</DefaultWorkspaceProvider>
</WorkspaceContext.Provider>
);
};
export const useWorkspace = () => useContext(WorkspaceContext);
This is my WorkspaceList.tsx:
import React, { createContext, useState, useContext, useEffect } from "react";
import last from "lodash/last";
import isEmpty from "lodash/isEmpty";
import { IWorkspaceDescriptor, IAnalyticalWorkspace } from "@gooddata/sdk-backend-spi/esm/workspace";
import { IPagedResource } from "@gooddata/sdk-backend-spi/esm/common/paging";
import { defaultSourceState, IWorkspaceSourceState } from "../utils";
import { workspaceFilter } from "../constants";
import { useBackend, useAuth } from "./Auth";
import { AuthStatus } from "./Auth/state";
export interface IWorkspaceListContext extends IWorkspaceSourceState {
firstWorkspace?: string;
}
const WorkspaceListContext = createContext<IWorkspaceListContext>({
...defaultSourceState,
});
const filterWorkspaces = (workspaces: IWorkspaceDescriptor[], filter?: RegExp) => {
if (filter) {
return workspaces.filter((workspace) => workspace.title.match(filter));
}
return workspaces;
};
const getFirstWorkspace = (workspaces: IWorkspaceDescriptor[]) => {
if (workspaces.length) {
return last(workspaces[0].id.split("/"));
}
return undefined;
};
export const WorkspaceListProvider: React.FC = ({ children }) => {
const { authStatus } = useAuth();
const backend = useBackend();
const [workspaceListState, setWorkspaceListState] = useState<IWorkspaceSourceState>({
...defaultSourceState,
});
const [firstWorkspace, setFirstWorkspace] = useState<string | undefined>(undefined);
useEffect(() => {
const getWorkspaces = async () => {
setWorkspaceListState({ isLoading: true });
try {
const workspaces: IWorkspaceDescriptor[] = [];
let page: IPagedResource<IAnalyticalWorkspace> = await backend
.workspaces()
.forCurrentUser()
.query();
while (!isEmpty(page.items)) {
const allDescriptors = await Promise.all(
page.items.map((workspace) => workspace.getDescriptor()),
);
workspaces.push(...allDescriptors);
page = await page.next();
}
const filteredWorkspaces = filterWorkspaces(workspaces, workspaceFilter);
setWorkspaceListState({
isLoading: false,
data: filteredWorkspaces,
});
setFirstWorkspace(getFirstWorkspace(filteredWorkspaces));
console.log('TAG WorkspaceList: ', getFirstWorkspace(filteredWorkspaces))
} catch (error) {
setWorkspaceListState({ isLoading: false });
}
};
setWorkspaceListState({ isLoading: false });
if (authStatus === AuthStatus.AUTHORIZED) {
getWorkspaces().catch(console.error);
}
}, [authStatus, backend]);
return (
<WorkspaceListContext.Provider value={{ ...workspaceListState, firstWorkspace }}>
{children}
</WorkspaceListContext.Provider>
);
};
export const useWorkspaceList = () => useContext(WorkspaceListContext);
this is my App.tsx:
import { BackendProvider } from "@gooddata/sdk-ui";
import { Provider } from "react-redux";
import { CookiesProvider } from "react-cookie";
import { PersistGate } from "redux-persist/integration/react";
import AppRouter from "./routes/AppRouter";
import { useAuth } from "./contexts/Auth";
import { WorkspaceListProvider } from "./contexts/WorkspaceList";
import { store, persistor } from "./redux/store";
import { FbAuthContextProvider } from "./contexts/FirebaseAuth/FirebaseAuthContext";
import { fbInit } from "./services/FirebaseServices";
import { GraphQLClient, ClientContext } from "graphql-hooks";
function App() {
const { backend } = useAuth();
fbInit();
const client = new GraphQLClient({
url: "<https://graphql.datocms.com/>",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: "Bearer 77253e9e549cbb257ad06a523ff55b",
},
});
return (
<CookiesProvider>
<FbAuthContextProvider>
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<BackendProvider backend={backend}>
<WorkspaceListProvider>
<ClientContext.Provider value={client}>
<AppRouter />
</ClientContext.Provider>
</WorkspaceListProvider>
</BackendProvider>
</PersistGate>
</Provider>
</FbAuthContextProvider>
</CookiesProvider>
);
}
export default App;
My web-app doesnt apply the workspace right away when the user logs in, I have to update the page in order for it to set the current loged in users workspace. Thank you in advance 🌟
j
Hello @Nicole Lopez! Could you please update your message and instead of using a single backtick (
Copy code
) on each line of code, could you use triple backtick (``
) for code block (i.e., per file)? 🙏 It would be much easier for us to read the code and copy/paste your files into our IDEs so that we can help. Thank you!
n
Hello @Jiri Zajic ofc I can, hang on 🌟
@Jiri Zajic this is my Workspace.tsx:
Copy code
import React, { createContext, useState, useContext, useEffect } from "react";
import { useQueryState } from "react-router-use-location-state";
import { WorkspaceProvider as DefaultWorkspaceProvider } from "@gooddata/sdk-ui";
import identity from "lodash/identity";

import { workspace as defaultWorkspace } from "../constants";

import { useWorkspaceList } from "./WorkspaceList";

interface IWorkspaceContext {
    workspace: string;
    setWorkspace: (workspace: string) => void;
}

export const WorkspaceContext = createContext<IWorkspaceContext>({
    workspace: defaultWorkspace,
    setWorkspace: identity,
});

export const WorkspaceProvider: React.FC = ({ children }) => {
    const workspaceList = useWorkspaceList();
    const [queryWorkspace, setQueryWorkspace] = useQueryState<string>("workspace", defaultWorkspace);
    const [workspace, setWorkspace] = useState<string>(queryWorkspace);

    // update query string with actual workspace
    useEffect(() => {
        setQueryWorkspace(workspace);
        //console.log("TAG query workspace: ", workspace);

        // Do not include setQueryWorkspace into effect dependencies
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [workspace, queryWorkspace]);

    // if workspace was not set yet then try to use first workspace available
    useEffect(() => {
        if (!workspace && workspaceList.firstWorkspace) {
            setWorkspace(workspaceList.firstWorkspace);
            //console.log("TAG first !!!workspace: ", workspaceList.firstWorkspace);
        }

        if (workspaceList.firstWorkspace) {
            //console.log("TAG workspaceList.firstWorkspace: ", workspaceList.firstWorkspace);

            setWorkspace(workspaceList.firstWorkspace);
        }
    }, [workspace, workspaceList]);

    return (
        <WorkspaceContext.Provider value={{ workspace, setWorkspace }}>
            <DefaultWorkspaceProvider workspace={workspace}>{children}</DefaultWorkspaceProvider>
        </WorkspaceContext.Provider>
    );
};

export const useWorkspace = () => useContext(WorkspaceContext);
@Jiri Zajic this is my WorkspaceList.tsx: import React, { createContext, useState, useContext, useEffect } from “react”; import last from “lodash/last”; import isEmpty from “lodash/isEmpty”; import { IWorkspaceDescriptor, IAnalyticalWorkspace } from “@gooddata/sdk-backend-spi/esm/workspace”; import { IPagedResource } from “@gooddata/sdk-backend-spi/esm/common/paging”; import { defaultSourceState, IWorkspaceSourceState } from “../utils”; import { workspaceFilter } from “../constants”; import { useBackend, useAuth } from “./Auth”; import { AuthStatus } from “./Auth/state”; export interface IWorkspaceListContext extends IWorkspaceSourceState { firstWorkspace?: string; } const WorkspaceListContext = createContext<IWorkspaceListContext>({ ...defaultSourceState, }); const filterWorkspaces = (workspaces: IWorkspaceDescriptor[], filter?: RegExp) => { if (filter) { return workspaces.filter((workspace) => workspace.title.match(filter)); } return workspaces; }; const getFirstWorkspace = (workspaces: IWorkspaceDescriptor[]) => { if (workspaces.length) { return last(workspaces[0].id.split(“/”)); } return undefined; }; export const WorkspaceListProvider: React.FC = ({ children }) => { const { authStatus } = useAuth(); const backend = useBackend(); const [workspaceListState, setWorkspaceListState] = useState<IWorkspaceSourceState>({ ...defaultSourceState, }); const [firstWorkspace, setFirstWorkspace] = useState<string | undefined>(undefined); useEffect(() => { const getWorkspaces = async () => { setWorkspaceListState({ isLoading: true }); try { const workspaces: IWorkspaceDescriptor[] = []; let page: IPagedResource<IAnalyticalWorkspace> = await backend .workspaces() .forCurrentUser() .query(); while (!isEmpty(page.items)) { const allDescriptors = await Promise.all( page.items.map((workspace) => workspace.getDescriptor()), ); workspaces.push(...allDescriptors); page = await page.next(); } const filteredWorkspaces = filterWorkspaces(workspaces, workspaceFilter); setWorkspaceListState({ isLoading: false, data: filteredWorkspaces, }); setFirstWorkspace(getFirstWorkspace(filteredWorkspaces)); } catch (error) { setWorkspaceListState({ isLoading: false }); } }; setWorkspaceListState({ isLoading: false }); if (authStatus === AuthStatus.AUTHORIZED) { getWorkspaces().catch(console.error); } }, [authStatus, backend]); return ( <WorkspaceListContext.Provider value={{ ...workspaceListState, firstWorkspace }}> {children} </WorkspaceListContext.Provider> ); }; export const useWorkspaceList = () => useContext(WorkspaceListContext);
@Jiri Zajic this is my App.tsx
Copy code
import { BackendProvider } from "@gooddata/sdk-ui";
import { Provider } from "react-redux";
import { CookiesProvider } from "react-cookie";
import { PersistGate } from "redux-persist/integration/react";

import AppRouter from "./routes/AppRouter";
import { useAuth } from "./contexts/Auth";
import { WorkspaceListProvider } from "./contexts/WorkspaceList";
import { store, persistor } from "./redux/store";
import { FbAuthContextProvider } from "./contexts/FirebaseAuth/FirebaseAuthContext";
import { fbInit } from "./services/FirebaseServices";
import { GraphQLClient, ClientContext } from "graphql-hooks";

function App() {
    const { backend } = useAuth();

    fbInit();

    const client = new GraphQLClient({
        url: "<https://graphql.datocms.com/>",
        headers: {
            Accept: "application/json",
            "Content-Type": "application/json",
            Authorization: "Bearer 77253e9e549cbb257ad06a523ff55b",
        },
    });

    return (
        <CookiesProvider>
            <FbAuthContextProvider>
                <Provider store={store}>
                    <PersistGate loading={null} persistor={persistor}>
                        <BackendProvider backend={backend}>
                            <WorkspaceListProvider>
                                <ClientContext.Provider value={client}>
                                    <AppRouter />
                                </ClientContext.Provider>
                            </WorkspaceListProvider>
                        </BackendProvider>
                    </PersistGate>
                </Provider>
            </FbAuthContextProvider>
        </CookiesProvider>
    );
}

export default App;
@Jiri Zajic and this is my router.tsx
Copy code
import React, { useState } from "react";
import { BrowserRouter as Router, Route, Redirect } from "react-router-dom";
import { useCookies } from "react-cookie";

import { WorkspaceProvider } from "../contexts/Workspace";
import Page from "../components/Page";

import Login from "./Login";
import Logout from "./Logout";
import Overview from "./Overview";
import ImagePerception from "./ImagePerception";
import Differentiation from "./Differentiation";
import SocialResponsibility from "./SocialResponsibility";
import Segmentation from "./Segmentation";
import KeyIndicators from "./KeyIndicators";
import Comparison from "./Comparison";

import styles from "./AppRouter.module.scss";

import { useAuth } from "../contexts/Auth";
import { AuthStatus } from "../contexts/Auth/state";
import FirebaseLoginForm from "../components/auth/FirebaseLoginForm";
import Demographics from "./Demographics";
import SurveyList from "./SurveyList";
import Introduction from "../components/Introduction/Introduction";

const RedirectIfNotLoggedIn: React.FC = () => {
    const auth = useAuth();
    const shouldRedirectToLogin = auth.authStatus === AuthStatus.UNAUTHORIZED;
    return shouldRedirectToLogin ? <Route component={() => <Redirect to="/login" />} /> : null;
};

const ShowIntroduction: React.FC = () => {
    const auth = useAuth();
    const isUserAuthed = auth.authStatus === AuthStatus.AUTHORIZED;
    const [cookies, setCookies] = useCookies(["introduction"]);
    const [showIntroduction, setShowIntroduction] = useState(cookies.introduction === "true" ? true : false);

    return showIntroduction && isUserAuthed ? (
        <Introduction
            callback={() => {
                setShowIntroduction(false);
                setCookies("introduction", false);
            }}
        />
    ) : null;
};

const AppRouter: React.FC = () => {
    return (
        <div className={styles.AppRouter}>
            <Router>
                {/* WorkspaceProvider depends on Router so it must be nested */}
                <WorkspaceProvider>
                    <>
                        <ShowIntroduction />
                        <Route exact path="/" component={SurveyList} />
                        <Route exact path="/overview" component={Overview} />

                        <Route exact path="/dashboard" component={() => <Page>Dashboard</Page>} />
                        <Route exact path="/login" component={Login} />
                        <Route exact path="/logout" component={Logout} />

                        {/* <Route exact path="/deep-dive" component={DeepDive} /> */}
                        <Route exact path="/differentiation" component={Differentiation} />
                        <Route exact path="/image-perception" component={ImagePerception} />
                        <Route exact path="/social-responsibility" component={SocialResponsibility} />
                        <Route exact path="/segmentation" component={Segmentation} />
                        <Route exact path="/key-indicators" component={KeyIndicators} />
                        <Route exact path="/demographics" component={Demographics} />
                        <Route exact path="/comparison" component={Comparison} />

                        <Route exact path="/firebaseloginform" component={FirebaseLoginForm} />

                        <RedirectIfNotLoggedIn />
                    </>
                </WorkspaceProvider>
            </Router>
        </div>
    );
};

export default AppRouter;
@Jiri Zajic @Dan Homola
j
Hello @Nicole Lopez! Sorry it took us so long. This seems like a more general React/coding issue rather than specific problem related to GoodData.UI, and therefore it was not our top priority. We are still determined to help you succeed though!
Let me start by saying that any user should be able to log in regardless if their workspace ID is in the constants.js or not. Even in a specific case where user has access to zero workspaces, he/she should still be able to authenticate.
I understand that your user is able to authenticate, but if he/she does not have access to the workspace defined in constants.js, then he/she ends up on a blank page. Correct? And this gets resolved after refreshing the page? Am I still right?
I believe that the problem could be that your code tries to load the page for the user too quickly; before it was determined what workspace should be used. I implement a similar logic long time ago in an older version of create-gooddata-react-app, and it's not publicly available, but I'll share two files that I think you could find helpful:
ProjectList.js (earlier, Workspace was called Project, so this is basically WorkspaceList.js):
Copy code
import React, { createContext, useState, useContext, useEffect } from "react";
import last from "lodash/last";

import { defaultSourceState } from "../utils";
import sdk from "../sdk";
import { useAuth } from "../contexts/Auth";
import { projectId as constProjectId, projectFilter } from "../constants";

const ProjectListContext = createContext({
    ...defaultSourceState,
    firstProjectId: null,
});

const filterProjects = (projects, filter) => {
    return !filter ? projects : projects.filter(project => project.meta.title.match(filter));
};

const getFirstProject = projects => {
    return projects.length && last(projects[0].links.self.split("/"));
};

const getFirstProjectInSegment = async (projects, segment = "ECommerce") => {
    let projectInSegment;

    for (const project of projects) {
        const projectId = last(project.links.self.split("/"));
        const result = await sdk.xhr.get(`/gdc/app/account/bootstrap?projectUri=/gdc/projects/${projectId}`);
        const projectSegment = result.data.bootstrapResource.current.projectLcm?.segmentId;
        if (segment === projectSegment) {
            projectInSegment = last(project.links.self.split("/"));
            break;
        }
    }

    return projectInSegment || getFirstProject(projects);
};

export const ProjectListProvider = ({ children }) => {
    const authState = useAuth();
    const [projectListState, setProjectListState] = useState({ ...defaultSourceState });
    const [firstProjectId, setFirstProjectId] = useState(null);

    useEffect(() => {
        const getProjects = async userId => {
            setProjectListState({ isLoading: true });
            try {
                const currentProjects = await sdk.project.getProjects(userId);
                const filteredProjects = filterProjects(currentProjects, projectFilter);
                setProjectListState({
                    isLoading: false,
                    data: filteredProjects,
                });
                if (!constProjectId) {
                    const projectInSegment = await getFirstProjectInSegment(filteredProjects);
                    setFirstProjectId(projectInSegment);
                }
            } catch (error) {
                setProjectListState({ isLoading: false, error });
            }
        };

        setProjectListState({ isLoading: false });
        if (!authState.isLoading && authState.data) getProjects(authState.data.loginMD5);
    }, [authState.isLoading, authState.data]);

    return (
        <ProjectListContext.Provider value={{ ...projectListState, firstProjectId }}>
            {children}
        </ProjectListContext.Provider>
    );
};

export const useProjectList = () => useContext(ProjectListContext);
MyPage.js (this is my custom implementation to replace original Page.js):
Copy code
import React, { useState, useEffect } from "react";
import Helmet from "react-helmet";
import { withRouter } from "react-router";
import { NavLink, Link } from "react-router-dom";
import cx from "classnames";
import Popover from "@material-ui/core/Popover";

import { useAuth } from "../contexts/Auth";
import { useProjectId } from "../contexts/ProjectId";
import sdk from "../sdk";
import { useLocalStorage } from "../utils";
import styles from "./MyPage.module.scss";
import defaultLogo from "../assets/logo.svg";

const MyPage = ({
    title = "GoodData Ecommerce Demo",
    className,
    navigation = true,
    backButton,
    location,
    children,
}) => {
    const authState = useAuth();
    const { projectId } = useProjectId();
    const [projectTitle, setProjectTitle] = useState("Determining workspace…");
    const [logo, setLogo] = useLocalStorage("logo", defaultLogo);
    const [logoForm, setlogoForm] = useState("");
    const [anchorEl, setAnchorEl] = useState(null);

    const handleClick = event => {
        setAnchorEl(event.currentTarget);
    };
    const handleClose = () => {
        setAnchorEl(null);
    };
    const open = Boolean(anchorEl);
    const id = open ? "simple-popover" : undefined;

    useEffect(() => {
        // TODO this ugliness is slow, pricy, and should be avoided (maybe by
        // storing both workspaceId and workspaceTitle when firstProjectInSegment
        // found in ProjectList.js)
        if (projectId) {
            sdk.xhr.get(`/gdc/app/account/bootstrap?projectUri=/gdc/projects/${projectId}`).then(result => {
                const title = result.data.bootstrapResource.current.project?.meta?.title || "Unknown";
                setProjectTitle(title);
            });
        }
    }, [projectId]);

    useEffect(() => {
        setlogoForm(logo);
    }, [logo]);

    return (
        <div className={cx(styles.Page)}>
            <Helmet>
                <title>{title}</title>
            </Helmet>
            <header className={styles.Header}>
                <Link to="/" className={styles.HeaderSvg} style={{ backgroundImage: `url(${logo})` }} />
                <ul className={styles.Menu}>
                    <li
                        className={cx(
                            (location.pathname === "/" || location.pathname.startsWith("/product")) &&
                                styles.Active,
                        )}
                    >
                        <NavLink to="/">Listings</NavLink>
                    </li>
                    <li className={cx(location.pathname.startsWith("/analytical-designer") && styles.Active)}>
                        <NavLink to="/analytical-designer">Analytical Designer</NavLink>
                    </li>
                    <li className={cx(location.pathname.startsWith("/kpis-and-alerts") && styles.Active)}>
                        <NavLink to="/kpis-and-alerts">KPIs & Alerts</NavLink>
                    </li>
                </ul>

                <div className={styles.Workspace} title={projectId}>
                    {projectTitle}
                </div>

                <div>
                    <Link to="/" className={styles.DocSvg} />
                    <Link onClick={handleClick} className={styles.PrintSvg} />
                    <Popover
                        id={id}
                        open={open}
                        anchorEl={anchorEl}
                        onClose={handleClose}
                        anchorOrigin={{
                            vertical: "bottom",
                            horizontal: "left",
                        }}
                        transformOrigin={{
                            vertical: "top",
                            horizontal: "left",
                        }}
                    >
                        <div style={{ margin: "16px" }}>
                            <form onSubmit={() => setLogo(logoForm)}>
                                <label>
                                    Logo URL:
                                    <br />
                                    <textarea
                                        rows="10"
                                        cols="50"
                                        type="text"
                                        value={logoForm}
                                        onChange={e => setlogoForm(e.target.value)}
                                    />
                                </label>
                                <br />
                                <br />
                                <input type="submit" value="Change" />
                                <button onClick={() => setLogo(defaultLogo)}>Default</button>
                            </form>
                        </div>
                    </Popover>
                </div>

                <div className={styles.User}>
                    <div className={styles.Avatar}></div>
                    <div className={styles.Info}>
                        <div className={styles.Name}>
                            <div>
                                {authState?.data
                                    ? `${authState?.data?.firstName} ${authState?.data?.lastName}`
                                    : "…"}
                            </div>
                        </div>
                    </div>
                </div>
            </header>
            <main className={styles.Main}>
                <div className={className}>{projectId && children}</div>
            </main>
        </div>
    );
};

export default withRouter(MyPage);
Please investigate thoroughly as this code works; it lets user log in, and then it loads the list of his/her available workspaces, and then it looks for a workspace that belongs to a specific segment. Once such workspace is found, it is "chosen" and the whole app loads with this chosen workspace in mind.
If you take a closer look at MyPage.js, around line 136 you'll see this:
<div className={className}>{projectId && children}</div>
. This is important as this ensures that the whole application WAITS until a correct workspace is set. It won't start rendering analytics unless the correct workspace has been found. From what you describe, I believe that if you also make your application wait until the desired workspace has been set it could resolve your problem of a user ending up on a blank page.
Please let me know if this helps! 🙏