Nicole Lopez
05/31/2022, 7:42 AMimport 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 🌟Jiri Zajic
06/01/2022, 6:46 AM) 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!Nicole Lopez
06/01/2022, 7:06 AMNicole Lopez
06/01/2022, 7:07 AMimport 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);
Nicole Lopez
06/01/2022, 7:07 AMNicole Lopez
06/01/2022, 7:08 AMimport { 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;
Nicole Lopez
06/01/2022, 7:10 AMimport 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;
Nicole Lopez
06/07/2022, 8:27 AMJiri Zajic
06/09/2022, 8:45 AMJiri Zajic
06/09/2022, 8:47 AMJiri Zajic
06/09/2022, 8:48 AMJiri Zajic
06/09/2022, 8:50 AMJiri Zajic
06/09/2022, 8:50 AMJiri Zajic
06/09/2022, 8:50 AMimport 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);
Jiri Zajic
06/09/2022, 8:51 AMJiri Zajic
06/09/2022, 8:51 AMimport 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);
Jiri Zajic
06/09/2022, 8:52 AMJiri Zajic
06/09/2022, 8:54 AM<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.Jiri Zajic
06/09/2022, 8:55 AM