diff --git a/src/App.tsx b/src/App.tsx index cfb5a44..10019fe 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,14 +1,7 @@ -import { - Admin, - EditGuesser, - ListGuesser, - Resource, - ShowGuesser, -} from "react-admin"; +import { Admin, Resource } from "react-admin"; import { Layout } from "./Layout"; -import { dataProvider } from "./dataProvider"; -import { authProvider } from "./authProvider"; -import { AdminDashboard } from '@admin'; +import { AdminDashboard, PersonList, PersonShow } from "@admin"; +import { authProvider, dataProvider } from "@core"; export const App = () => ( ( > ); diff --git a/src/admin-components/admin-dashboard.tsx b/src/admin-components/admin-dashboard.tsx index 13f98c0..4d4601c 100644 --- a/src/admin-components/admin-dashboard.tsx +++ b/src/admin-components/admin-dashboard.tsx @@ -1,20 +1,18 @@ -import { Card, CardContent } from "@mui/material"; -import { Title, useDataProvider } from "react-admin"; -import { useQuery } from "@tanstack/react-query"; +import { Card, CardContent, Typography } from "@mui/material"; +import { Title } from "react-admin"; export const AdminDashboard = () => { - const dataProvider = useDataProvider(); - const { data } = useQuery({ - queryKey: ["dashboard"], - queryFn: () => dataProvider.person(), - }); - - console.log(data); - return ( - + - <CardContent>Lorem ipsum sic dolor amet...</CardContent> + <CardContent> + <Typography> + Lorem ipsum dolor sit amet, consectetur adipisicing elit. Eveniet + impedit, temporibus? Amet dolor fuga hic libero molestiae nemo + perferendis quas quis repellendus voluptas? Aliquid doloremque, est + labore odit optio similique. + </Typography> + </CardContent> </Card> ); }; diff --git a/src/admin-components/admin-menu.tsx b/src/admin-components/admin-menu.tsx index c5d91d0..d229bff 100644 --- a/src/admin-components/admin-menu.tsx +++ b/src/admin-components/admin-menu.tsx @@ -3,5 +3,6 @@ import { Menu } from "react-admin"; export const AdminMenu = () => ( <Menu> <Menu.DashboardItem /> + <Menu.ResourceItem name="person" /> </Menu> ); diff --git a/src/admin-components/index.ts b/src/admin-components/index.ts index 21bcece..fd665ba 100644 --- a/src/admin-components/index.ts +++ b/src/admin-components/index.ts @@ -1,2 +1,3 @@ export { AdminMenu } from './admin-menu'; export { AdminDashboard } from './admin-dashboard'; +export * from './resources'; diff --git a/src/admin-components/resources/index.ts b/src/admin-components/resources/index.ts new file mode 100644 index 0000000..6e3bd67 --- /dev/null +++ b/src/admin-components/resources/index.ts @@ -0,0 +1 @@ +export * from './person'; diff --git a/src/admin-components/resources/person/components/array-field.tsx b/src/admin-components/resources/person/components/array-field.tsx new file mode 100644 index 0000000..7885319 --- /dev/null +++ b/src/admin-components/resources/person/components/array-field.tsx @@ -0,0 +1,31 @@ +import React, { FC } from 'react'; +import { isObject } from "lodash"; +import { useRecordContext } from 'react-admin'; +import { Chip, Stack } from '@mui/material'; + +interface FieldProps { + source: string; +} + +export const ArrayField: FC<FieldProps> = ({ source }) => { + const record = useRecordContext(); + const objects = record?.[source]; + + if (!objects) return ""; + + return ( + <Stack direction="row" gap={1}> + {objects.map((currentObject: string, key: number) => { + if (isObject(currentObject)) { + return ( + <Chip key={`${key}-people`} size="small" label={currentObject.name} /> + ); + } + + return ( + <Chip key={`${key}-people`} size="small" label={currentObject} /> + ); + })} + </Stack> + ); +}; diff --git a/src/admin-components/resources/person/components/index.ts b/src/admin-components/resources/person/components/index.ts new file mode 100644 index 0000000..214238c --- /dev/null +++ b/src/admin-components/resources/person/components/index.ts @@ -0,0 +1 @@ +export { ArrayField } from "./array-field.tsx"; diff --git a/src/admin-components/resources/person/index.ts b/src/admin-components/resources/person/index.ts new file mode 100644 index 0000000..747170b --- /dev/null +++ b/src/admin-components/resources/person/index.ts @@ -0,0 +1,3 @@ +export { PersonList } from './list.tsx'; +export { PersonShow } from './show.tsx'; +export * from './components'; diff --git a/src/admin-components/resources/person/list.tsx b/src/admin-components/resources/person/list.tsx new file mode 100644 index 0000000..5f76792 --- /dev/null +++ b/src/admin-components/resources/person/list.tsx @@ -0,0 +1,103 @@ +import React, { Fragment, useEffect, useState } from "react"; +import { + BooleanField, + Datagrid, + isEmpty, + ListContextProvider, + ListToolbar, + Pagination, + TextField, + Title, + TopToolbar, + useDataProvider, + WrapperField, +} from "react-admin"; +import { useQuery } from "@tanstack/react-query"; +import { ArrayField } from "./components"; +import { TextField as MuiTextField } from "@mui/material"; + +export const PersonList = () => { + const [page, setPage] = useState(1); + const [perPage, setPerPage] = useState(25); + const [firstName, setFirstName] = useState(""); + const dataProvider = useDataProvider(); + const { data, isPending, refetch } = useQuery({ + queryKey: ["personList", page, perPage, firstName], + queryFn: () => { + const httpParams = new URLSearchParams({ + page: `${page - 1}`, + size: `${perPage}`, + ...(isEmpty(firstName) + ? {} + : { filters: `firstName::like_ignore_case::${firstName}` }), + }); + + return dataProvider.personList(httpParams); + }, + }); + + useEffect(() => { + (async () => { + await refetch(); + })(); + }, [page, perPage]); + + return ( + <Fragment> + <Title title="Users" /> + <ListToolbar + actions={ + <TopToolbar> + <FilterName setFirstName={setFirstName} /> + </TopToolbar> + } + /> + <Datagrid + resource="person" + data={data?.content} + total={data?.totalElements} + isPending={isPending} + sort={undefined} + bulkActionButtons={false} + > + <TextField source="firstName" /> + <TextField source="lastName" /> + <TextField source="email" label="Email User ID" /> + <TextField source="id" label="User ID" /> + <TextField source="status" /> + <BooleanField source="userAccess" label="Access to the Platform" /> + <WrapperField label="Company"> + <ArrayField source="parties" /> + </WrapperField> + <WrapperField label="Programs assigned"> + <ArrayField source="assignedPrograms" /> + </WrapperField> + <WrapperField label="Functions assigned"> + <ArrayField source="assignedFunctions" /> + </WrapperField> + </Datagrid> + <ListContextProvider + value={{ + page, + perPage, + total: data?.totalElements ?? 0, + setPage, + setPerPage, + }} + > + <Pagination /> + </ListContextProvider> + </Fragment> + ); +}; + +const FilterName = ({ setFirstName }) => { + return ( + <MuiTextField + label="First name" + onChange={(event: React.ChangeEvent<HTMLInputElement>) => { + setFirstName(event.target.value); + }} + /> + ); +}; diff --git a/src/admin-components/resources/person/show.tsx b/src/admin-components/resources/person/show.tsx new file mode 100644 index 0000000..56c6343 --- /dev/null +++ b/src/admin-components/resources/person/show.tsx @@ -0,0 +1,114 @@ +import React from "react"; +import { + BooleanField, + DateField, + Labeled, + Show, + SimpleShowLayout, + TextField, +} from "react-admin"; +import { + Box, + Card, + CardContent, + CardHeader, + Stack, + Typography, +} from "@mui/material"; +import { ArrayField } from "@admin"; + +export const PersonShow = () => { + return ( + <Show> + <SimpleShowLayout> + <Card sx={{ border: 1 }}> + <CardHeader title="General info" /> + <CardContent> + <Stack + sx={{ + flexWrap: "wrap", + justifyContent: "flex-start", + alignItems: "center", + }} + direction="row" + columnGap={5} + rowGap={2} + > + <Labeled label="Name"> + <Stack direction="row" gap={1}> + <TextField sx={{ textTransform: "" }} source="title" /> + <TextField source="firstName" /> + <TextField source="lastName" /> + </Stack> + </Labeled> + <Labeled label="E-mail address"> + <TextField source="email" /> + </Labeled> + <Labeled label="User ID"> + <TextField source="id" /> + </Labeled> + <Labeled label="Access to the Platform"> + <BooleanField source="userAccess" /> + </Labeled> + <Box> + <Typography + sx={{ color: "rgba(255, 255, 255, 0.7)" }} + variant="caption" + > + Company + </Typography> + <ArrayField source="parties" /> + </Box> + <Box sx={{ flexBasis: "100%" }} /> + <Labeled label="Access to the Platform"> + <BooleanField source="userAccess" /> + </Labeled> + <Labeled label="Position"> + <TextField source="jobPosition" emptyText={"\u2014"} /> + </Labeled> + <Labeled label="Phone number"> + <TextField source="phoneNumber" emptyText={"\u2014"} /> + </Labeled> + <Labeled label="Mobile number"> + <TextField source="mobileNumber" emptyText={"\u2014"} /> + </Labeled> + <Box /> + </Stack> + </CardContent> + </Card> + <Card sx={{ border: 1 }}> + <CardHeader title="Platform info" /> + <CardContent> + <Stack + sx={{ justifyContent: "flex-start", alignItems: "center" }} + direction="row" + gap={5} + > + <Labeled label="Last changes date"> + <DateField + source="updateDate" + locales="fr-CA" + emptyText={"\u2014"} + /> + </Labeled> + <Labeled label="User Access Active From"> + <DateField + source="userAccessFrom" + locales="fr-CA" + emptyText={"\u2014"} + /> + </Labeled> + <Labeled label="User Access Active To"> + <DateField + source="userAccessTo" + locales="fr-CA" + emptyText={"\u2014"} + /> + </Labeled> + </Stack> + </CardContent> + </Card> + </SimpleShowLayout> + </Show> + ); +}; diff --git a/src/authProvider.ts b/src/admin-core/authProvider.ts similarity index 89% rename from src/authProvider.ts rename to src/admin-core/authProvider.ts index 6988dce..cbaad34 100644 --- a/src/authProvider.ts +++ b/src/admin-core/authProvider.ts @@ -10,6 +10,7 @@ export const authProvider: AuthProvider = { response = await fetch( new Request("http://localhost:8080/atsp-idp/token", { method: "POST", + credentials: "include", body: new URLSearchParams({ grant_type: "authorization_code", code: "code", @@ -36,9 +37,9 @@ export const authProvider: AuthProvider = { ); } - const { refresh_token } = await response.json(); - localStorage.setItem("token", refresh_token); - localStorage.setItem("user", refresh_token); + const { access_token } = await response.json(); + localStorage.setItem("user", access_token); + localStorage.setItem("token", access_token); return Promise.resolve(); }, diff --git a/src/admin-core/data-provider-extension.ts b/src/admin-core/data-provider-extension.ts index 7e2209c..9b4fe62 100644 --- a/src/admin-core/data-provider-extension.ts +++ b/src/admin-core/data-provider-extension.ts @@ -1,19 +1,10 @@ -import { useQueryEngine } from "@core"; +import { useQueryEngine } from './hooks'; export const dataProviderExtension = () => { const { fetchCommon } = useQueryEngine(); + const url = import.meta.env.VITE_SIMPLE_REST_URL; return { - person: () => - fetchCommon( - "http://localhost:8081/core/api/admin/person", - { - headers: new Headers({ - Accept: "application/json", - 'X-XSRF-TOKEN': 'c3aKjTfLsPSdmiMtTaj933RbsQMR6IWmf9C5ZImMh9pCmDGkEETougKpiZew-UcZe4XJ5xE4nDpwirWLR-SOXbm94-Im-QLB', - 'Cookie': 'LAST_LOGIN_LOCATION=http://localhost:3000; SESSION=652f7c23-fe94-4727-a980-59052e75aab2; XSRF-TOKEN=c2b75b9c-cd46-48ec-9ab0-847901d8da3e', - }), - }, - ), + personList: (params: URLSearchParams) => fetchCommon(`${url}/person?${params}`), }; }; diff --git a/src/admin-core/dataProvider.ts b/src/admin-core/dataProvider.ts new file mode 100644 index 0000000..e89abe4 --- /dev/null +++ b/src/admin-core/dataProvider.ts @@ -0,0 +1,31 @@ +import { fetchUtils, withLifecycleCallbacks } from 'react-admin'; +import simpleRestProvider from "ra-data-simple-rest"; +import { dataProviderExtension } from "@core"; + +const fetchJson = (url, options = {}) => { + if (!options.headers) { + options.headers = new Headers({ Accept: "application/json" }); + } + options.credentials = "include"; + options.headers.set( + "Authorization", + `Bearer ${localStorage.getItem("user")}`, + ); + options.headers.set("X-XSRF-TOKEN", localStorage.getItem("user")); + return fetchUtils.fetchJson(url, options); +}; + +export const dataProvider = withLifecycleCallbacks( + { + ...simpleRestProvider(import.meta.env.VITE_SIMPLE_REST_URL, fetchJson), + ...dataProviderExtension(), + }, + [ + { + resource: "person", + afterRead: async (data: object) => { + return data.original; + }, + }, + ], +); diff --git a/src/admin-core/hooks/useQueryEngine.ts b/src/admin-core/hooks/useQueryEngine.ts index c1d1197..752b6c8 100644 --- a/src/admin-core/hooks/useQueryEngine.ts +++ b/src/admin-core/hooks/useQueryEngine.ts @@ -1,16 +1,16 @@ export const useQueryEngine = () => { const defaultHeaders = { - Accept: 'application/ld+json', - 'Content-Type': 'application/ld+json', + Accept: "application/json", + "Content-Type": "application/json", }; - const response = async (request) => { + const response = async (request: Request) => { let response; try { response = await fetch(request); } catch (e) { - throw new Error('Network error'); + throw new Error("Network error"); } if (200 > response?.status || 300 <= response?.status) { @@ -20,58 +20,76 @@ export const useQueryEngine = () => { return response.json(); }; - return ({ + const decodeJwt = (token: string) => { + try { + return JSON.parse(window.atob(token.split(".")[1])); + } catch (e) { + return null; + } + }; + + return { defaultHeaders, - fetchCommon: async (url, options = {}) => { + fetchCommon: async (url: string, options: object = {}) => { + const token = decodeJwt(localStorage.getItem("user")); + + document.cookie = `XSRF-TOKEN=${token.sub}; SESSION=${token.jti}`; + return await response( new Request(url, { - ...(options?.method ? { method: options.method } : { method: 'GET' }), + credentials: "include", + ...(options?.method ? { method: options.method } : { method: "GET" }), ...(options.replaceHeaders - ? { headers: options.replaceHeaders } + ? { headers: new Headers(options.replaceHeaders) } : { - headers: { - ...defaultHeaders, - Authorization: `Bearer ${localStorage.getItem('user')}`, - ...(options?.headers ? { ...options.headers } : {}), - }, - }), + headers: new Headers({ + ...defaultHeaders, + Authorization: `Bearer ${localStorage.getItem("user")}`, + "X-XSRF-TOKEN": localStorage.getItem("user"), + ...(options?.headers ? { ...options.headers } : {}), + }), + }), ...(options?.body ? { body: options.body } : {}), }), ); }, - fetchMultipart: async (url, options) => { + fetchMultipart: async (url: string, options: object = {}) => { return await response( new Request(url, { - method: 'POST', + method: "POST", headers: { - Accept: 'application/ld+json', + Accept: "application/json", /** * DO NOT SPECIFY THIS - Because the boundary data in it * @see https://stackoverflow.com/a/71392989/3111514 */ // 'Content-Type': 'multipart/form-data', - Authorization: `Bearer ${localStorage.getItem('user')}`, + Authorization: `Bearer ${localStorage.getItem("user")}`, }, ...options, }), ); }, - fetchPlain: async (url, options = {}, withAuth = false) => { + fetchPlain: async (url: string, options: object = {}, withAuth = false) => { return await response( new Request(url, { - ...(options?.method ? { method: options.method } : { method: 'GET' }), + ...(options?.method ? { method: options.method } : { method: "GET" }), ...(options.replaceHeaders ? { headers: options.replaceHeaders } : { - headers: { - 'Content-Type': 'application/json', - ...(options?.headers ? { headers: options.headers } : {}), - ...(withAuth ? { Authorization: `Bearer ${localStorage.getItem('user')}` } : {}), - }, - }), + headers: { + "Content-Type": "application/json", + ...(options?.headers ? { headers: options.headers } : {}), + ...(withAuth + ? { + Authorization: `Bearer ${localStorage.getItem("user")}`, + } + : {}), + }, + }), ...(options?.body ? { body: options.body } : {}), }), ); }, - }); + }; }; diff --git a/src/admin-core/index.ts b/src/admin-core/index.ts index 64c0369..bbdbaa9 100644 --- a/src/admin-core/index.ts +++ b/src/admin-core/index.ts @@ -1,2 +1,4 @@ export { dataProviderExtension } from './data-provider-extension'; +export { dataProvider } from './dataProvider.ts'; +export { authProvider } from './authProvider.ts'; export * from './hooks'; diff --git a/src/dataProvider.ts b/src/dataProvider.ts deleted file mode 100644 index 122178f..0000000 --- a/src/dataProvider.ts +++ /dev/null @@ -1,7 +0,0 @@ -import simpleRestProvider from "ra-data-simple-rest"; -import { dataProviderExtension } from "@core"; - -export const dataProvider = { - ...simpleRestProvider(import.meta.env.VITE_SIMPLE_REST_URL), - ...dataProviderExtension(), -};