From 44d9ad71f06aacec8e2110086b21d085df3a3c23 Mon Sep 17 00:00:00 2001 From: Javi Pacheco Date: Mon, 18 Sep 2023 10:58:35 +0200 Subject: [PATCH] Organization screen (#431) * First approach for organization screen * First approach showing list of organizations * Adding and updating organizations * Deleting organizations from UI --- .../com/xebia/functional/xef/server/Server.kt | 2 + server/web/src/components/Login/Login.tsx | 4 +- .../Pages/FeatureOne/FeatureOne.tsx | 3 - .../src/components/Pages/FeatureOne/index.ts | 1 - .../Pages/Organizations/Organizations.tsx | 229 ++++++++++++++++++ .../components/Pages/Organizations/index.ts | 1 + server/web/src/components/Sidebar/Sidebar.tsx | 4 +- server/web/src/main.tsx | 6 +- server/web/src/utils/api/config.ts | 23 +- server/web/src/utils/api/login.ts | 51 ---- server/web/src/utils/api/organizations.ts | 138 +++++++++++ server/web/src/utils/api/register.ts | 54 ----- server/web/src/utils/api/users.ts | 109 +++++++++ server/web/src/utils/models.ts | 10 + 14 files changed, 516 insertions(+), 119 deletions(-) delete mode 100644 server/web/src/components/Pages/FeatureOne/FeatureOne.tsx delete mode 100644 server/web/src/components/Pages/FeatureOne/index.ts create mode 100644 server/web/src/components/Pages/Organizations/Organizations.tsx create mode 100644 server/web/src/components/Pages/Organizations/index.ts delete mode 100644 server/web/src/utils/api/login.ts create mode 100644 server/web/src/utils/api/organizations.ts delete mode 100644 server/web/src/utils/api/register.ts create mode 100644 server/web/src/utils/api/users.ts diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/Server.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/Server.kt index dc6146a85..42ef4bbf8 100644 --- a/server/src/main/kotlin/com/xebia/functional/xef/server/Server.kt +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/Server.kt @@ -19,6 +19,7 @@ import io.ktor.client.engine.cio.* import io.ktor.client.plugins.contentnegotiation.ContentNegotiation as ClientContentNegotiation import io.ktor.client.plugins.auth.* import io.ktor.client.plugins.logging.* +import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* import io.ktor.server.application.* import io.ktor.server.auth.* @@ -66,6 +67,7 @@ object Server { server(factory = Netty, port = 8081, host = "0.0.0.0") { install(CORS) { allowNonSimpleContentTypes = true + HttpMethod.DefaultMethods.forEach { allowMethod(it) } allowHeaders { true } anyHost() } diff --git a/server/web/src/components/Login/Login.tsx b/server/web/src/components/Login/Login.tsx index dfcab96f8..f4ebe45ec 100644 --- a/server/web/src/components/Login/Login.tsx +++ b/server/web/src/components/Login/Login.tsx @@ -4,11 +4,11 @@ import { useContext, useState } from 'react'; import { Navigate, useLocation, useNavigate } from 'react-router-dom'; import styles from './Login.module.css'; import { LoadingContext } from '@/state/Loading'; -import { postLogin } from '@/utils/api/login'; +import { postLogin } from '@/utils/api/users'; import { FormLogin } from './FormLogin'; import { FormRegister } from './FormRegister'; import { isValidEmail } from '@/utils/validate'; -import { postRegister } from '@/utils/api/register'; +import { postRegister } from '@/utils/api/users'; export function RequireAuth({ children }: { children: JSX.Element }) { let auth = useAuth(); diff --git a/server/web/src/components/Pages/FeatureOne/FeatureOne.tsx b/server/web/src/components/Pages/FeatureOne/FeatureOne.tsx deleted file mode 100644 index 8d87f85bb..000000000 --- a/server/web/src/components/Pages/FeatureOne/FeatureOne.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export function FeatureOne() { - return <>Feature One; -} diff --git a/server/web/src/components/Pages/FeatureOne/index.ts b/server/web/src/components/Pages/FeatureOne/index.ts deleted file mode 100644 index 2a8ecc38e..000000000 --- a/server/web/src/components/Pages/FeatureOne/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './FeatureOne'; diff --git a/server/web/src/components/Pages/Organizations/Organizations.tsx b/server/web/src/components/Pages/Organizations/Organizations.tsx new file mode 100644 index 000000000..7c9f08ad3 --- /dev/null +++ b/server/web/src/components/Pages/Organizations/Organizations.tsx @@ -0,0 +1,229 @@ +import { useAuth } from "@/state/Auth"; +import { LoadingContext } from "@/state/Loading"; +import { PostOrganizationProps, deleteOrganizations, getOrganizations, postOrganizations, putOrganizations } from "@/utils/api/organizations"; +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Grid, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TextField, + Typography +} from "@mui/material"; +import { ChangeEvent, useContext, useEffect, useState } from "react"; + +export function Organizations() { + + const auth = useAuth(); + + // functions to open/close the add/edit organization dialog + + const [openAddEditOrganization, setOpenAddEditOrganization] = useState(false); + + const [organizationforUpdating, setOrganizationForUpdating] = useState(undefined); // when adding is undefined and updating has the organization + + const [organizationDataInDialog, setOrganizationDataInDialog] = useState(undefined); + + const handleClickAddOrganization = () => { + setOpenAddEditOrganization(true); + setOrganizationForUpdating(undefined); + setOrganizationDataInDialog(undefined); + }; + + const handleClickEditOrganization = (org: OrganizationResponse) => { + setOpenAddEditOrganization(true); + setOrganizationForUpdating(org); + setOrganizationDataInDialog({ name: org.name }) + }; + + const handleCloseAddEditOrganization = () => { + setOpenAddEditOrganization(false); + }; + + const handleSaveAddEditOrganization = async () => { + if (organizationDataInDialog != undefined) { + if (organizationforUpdating == undefined) + await postOrganizations(auth.authToken, organizationDataInDialog); + else + await putOrganizations(auth.authToken, organizationforUpdating.id, organizationDataInDialog); + + loadOrganizations(); + } + setOpenAddEditOrganization(false); + }; + + const nameEditingHandleChange = (event: ChangeEvent) => { + setOrganizationDataInDialog({ name: event.target.value }) + }; + + // functions to open/close the delete organization dialog + + const [openDeleteOrganization, setOpenDeleteOrganization] = useState(false); + + const [organizationforDeleting, setOrganizationForDeleting] = useState(undefined); + + const handleClickDeleteOrganization = (org: OrganizationResponse) => { + setOpenDeleteOrganization(true); + setOrganizationForDeleting(org); + }; + + const handleCloseDeleteOrganzation = () => { + setOpenDeleteOrganization(false); + }; + + const handleDeleteOrganization = async () => { + if (organizationforDeleting != undefined) + await deleteOrganizations(auth.authToken, organizationforDeleting.id); + + loadOrganizations(); + + setOpenDeleteOrganization(false); + }; + + // function to load organizations + + const [loading, setLoading] = useContext(LoadingContext); + + const [organizations, setOrganizations] = useState([]); + + async function loadOrganizations() { + setLoading(true); + const response = await getOrganizations(auth.authToken); + setOrganizations(response); + setLoading(false); + } + + useEffect(() => { + loadOrganizations() + }, []); + + return <> + {/* List of organizations */} + {loading ? + <> + + Loading... + + : + <> + + + + Organizations + + + + + + + + + Users + Name + + + + + + {organizations.map((organization) => ( + + + {organization.users} + + + {organization.name} + + + + + + + + + ))} + +
+
+ + } + + {/* Add/Edit Organization Dialog */} + + + {organizationforUpdating == undefined ? "New Organization" : "Update Organization"} + + + + + + + + + + {/* Delete Organization Dialog */} + +
+ + + {"Delete Organizaction?"} + + + + Are you sure you want to delete this organization? + + + + + + + +
+ ; +} diff --git a/server/web/src/components/Pages/Organizations/index.ts b/server/web/src/components/Pages/Organizations/index.ts new file mode 100644 index 000000000..bb64ab3ba --- /dev/null +++ b/server/web/src/components/Pages/Organizations/index.ts @@ -0,0 +1 @@ +export * from './Organizations'; diff --git a/server/web/src/components/Sidebar/Sidebar.tsx b/server/web/src/components/Sidebar/Sidebar.tsx index 6603a31ef..3260c07a5 100644 --- a/server/web/src/components/Sidebar/Sidebar.tsx +++ b/server/web/src/components/Sidebar/Sidebar.tsx @@ -39,8 +39,8 @@ export function Sidebar({ drawerWidth, open }: SidebarProps) { - - + + diff --git a/server/web/src/main.tsx b/server/web/src/main.tsx index 52bef2253..321305af2 100644 --- a/server/web/src/main.tsx +++ b/server/web/src/main.tsx @@ -7,7 +7,7 @@ import { ThemeProvider } from '@emotion/react'; import { App } from '@/components/App'; import { Root } from '@/components/Pages/Root'; import { ErrorPage } from '@/components/Pages/ErrorPage'; -import { FeatureOne } from '@/components/Pages/FeatureOne'; +import { Organizations } from '@/components/Pages/Organizations'; import { Chat } from '@/components/Pages/Chat'; import { GenericQuestion } from '@/components/Pages/GenericQuestion'; import { SettingsPage } from '@/components/Pages/SettingsPage'; @@ -46,10 +46,10 @@ const router = createBrowserRouter([ ), }, { - path: '1', + path: 'organizations', element: ( - + ), }, diff --git a/server/web/src/utils/api/config.ts b/server/web/src/utils/api/config.ts index 980a57a37..4c5716b25 100644 --- a/server/web/src/utils/api/config.ts +++ b/server/web/src/utils/api/config.ts @@ -25,6 +25,7 @@ export type ApiOptions = { export enum EndpointsEnum { login = 'login', register = 'register', + organization = 'v1/settings/org', } export type EndpointsTypes = { @@ -89,9 +90,14 @@ const isErrorResponse = (b: unknown): b is ErrorResponse => { return (b as ErrorResponse).error !== undefined; }; +export type ResponseData = { + status: number; + data?: T; +}; + export async function apiFetch>( userApiConfig: ApiConfig, -): Promise { +): Promise> { const apiConfig = { ...userApiConfig, options: { @@ -100,14 +106,25 @@ export async function apiFetch>( }, }; + let response: Response | undefined = undefined + try { - const response = await fetchWithTimeout(apiConfig.url, apiConfig.options); + response = await fetchWithTimeout(apiConfig.url, apiConfig.options); const responseData: T | ErrorResponse = await response.json(); if (isErrorResponse(responseData)) throw responseData.error.message; - return responseData; + return { + status: response.status, + data: responseData, + }; } catch (error) { + if (response != undefined) { + return { + status: response.status, + data: undefined, + }; + } const errorMessage = `💢 Error: ${error}`; console.error(errorMessage); throw errorMessage; diff --git a/server/web/src/utils/api/login.ts b/server/web/src/utils/api/login.ts deleted file mode 100644 index dcd76933e..000000000 --- a/server/web/src/utils/api/login.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { - ApiOptions, - EndpointsEnum, - apiConfigConstructor, - apiFetch, - baseHeaders, - defaultApiServer, - } from '@/utils/api'; - - const loginApiBaseOptions: ApiOptions = { - endpointServer: defaultApiServer, - endpointPath: EndpointsEnum.login, - endpointValue: '', - requestOptions: { - method: 'POST', - headers: baseHeaders, - }, - }; - - export type PostLoginProps = { - email: string; - password: string; - }; - - export async function postLogin({ - email, - password, - }: PostLoginProps): Promise { - const loginRequest: LoginRequest = { - email: email, - password: password, - }; - const loginApiOptions: ApiOptions = { - ...loginApiBaseOptions, - body: JSON.stringify(loginRequest), - requestOptions: { - ...loginApiBaseOptions.requestOptions, - headers: { - ...loginApiBaseOptions.requestOptions?.headers, - }, - }, - }; - const loginApiConfig = apiConfigConstructor( - loginApiOptions, - ); - const loginResponse = await apiFetch( - loginApiConfig, - ); - - return loginResponse; - } diff --git a/server/web/src/utils/api/organizations.ts b/server/web/src/utils/api/organizations.ts new file mode 100644 index 000000000..d788139b8 --- /dev/null +++ b/server/web/src/utils/api/organizations.ts @@ -0,0 +1,138 @@ +import { + ApiOptions, + EndpointsEnum, + apiConfigConstructor, + apiFetch, + baseHeaders, + defaultApiServer, +} from '@/utils/api'; + +// Create Organization Endpoint + +const orgApiBaseOptions: ApiOptions = { + endpointServer: defaultApiServer, + endpointPath: EndpointsEnum.organization, + endpointValue: '', + requestOptions: { + headers: baseHeaders, + }, +}; + +export type PostOrganizationProps = { + name: string; +}; + +export async function postOrganizations(authToken: string, { + name, +}: PostOrganizationProps): Promise { + const createOrganizationRequest: OrganizationRequest = { + name: name, + }; + const createOrganizationApiOptions: ApiOptions = { + ...orgApiBaseOptions, + body: JSON.stringify(createOrganizationRequest), + requestOptions: { + method: 'POST', + ...orgApiBaseOptions.requestOptions, + headers: { + ...orgApiBaseOptions.requestOptions?.headers, + Authorization: `Bearer ${authToken}`, + }, + }, + }; + const createOrganizationApiConfig = apiConfigConstructor( + createOrganizationApiOptions, + ); + const createOrganizationResponse = await apiFetch( + createOrganizationApiConfig, + ); + + return createOrganizationResponse.status; +} + +// Get Organizations Endpoint + +export async function getOrganizations(authToken: string): Promise { + console.info('getOrganizations'); + const getOrganizationApiOptions: ApiOptions = { + ...orgApiBaseOptions, + requestOptions: { + method: 'GET', + ...orgApiBaseOptions.requestOptions, + headers: { + ...orgApiBaseOptions.requestOptions?.headers, + Authorization: `Bearer ${authToken}`, + }, + }, + }; + const getOrganizationApiConfig = apiConfigConstructor( + getOrganizationApiOptions, + ); + + const organizationsResponse = await apiFetch( + getOrganizationApiConfig, + ); + + if (organizationsResponse.data == null) { + throw new Error('Organizations data is null'); + } + + return organizationsResponse.data!; +} + +// put organization endpoint + +export async function putOrganizations(authToken: string, id: number, { + name, +}: PostOrganizationProps): Promise { + const putOrganizationRequest: OrganizationRequest = { + name: name, + }; + const putOrganizationApiOptions: ApiOptions = { + ...orgApiBaseOptions, + endpointValue: `/${id}`, + body: JSON.stringify(putOrganizationRequest), + requestOptions: { + method: 'PUT', + ...orgApiBaseOptions.requestOptions, + headers: { + ...orgApiBaseOptions.requestOptions?.headers, + Authorization: `Bearer ${authToken}`, + }, + }, + }; + const putOrganizationApiConfig = apiConfigConstructor( + putOrganizationApiOptions, + ); + const putOrganizationResponse = await apiFetch( + putOrganizationApiConfig, + ); + + return putOrganizationResponse.status; +} + + +// delete organization endpoint + +export async function deleteOrganizations(authToken: string, id: number): Promise { + const deleteOrganizationApiOptions: ApiOptions = { + ...orgApiBaseOptions, + endpointValue: `/${id}`, + requestOptions: { + method: 'DELETE', + ...orgApiBaseOptions.requestOptions, + headers: { + ...orgApiBaseOptions.requestOptions?.headers, + Authorization: `Bearer ${authToken}`, + }, + }, + }; + const deleteOrganizationApiConfig = apiConfigConstructor( + deleteOrganizationApiOptions, + ); + const deleteOrganizationResponse = await apiFetch( + deleteOrganizationApiConfig, + ); + + return deleteOrganizationResponse.status; +} diff --git a/server/web/src/utils/api/register.ts b/server/web/src/utils/api/register.ts deleted file mode 100644 index 396ab6144..000000000 --- a/server/web/src/utils/api/register.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { - ApiOptions, - EndpointsEnum, - apiConfigConstructor, - apiFetch, - baseHeaders, - defaultApiServer, - } from '@/utils/api'; - - const registerApiBaseOptions: ApiOptions = { - endpointServer: defaultApiServer, - endpointPath: EndpointsEnum.register, - endpointValue: '', - requestOptions: { - method: 'POST', - headers: baseHeaders, - }, - }; - - export type PostRegisterProps = { - name: string; - email: string; - password: string; - }; - - export async function postRegister({ - name, - email, - password, - }: PostRegisterProps): Promise { - const registerRequest: RegisterRequest = { - name: name, - email: email, - password: password, - }; - const registerApiOptions: ApiOptions = { - ...registerApiBaseOptions, - body: JSON.stringify(registerRequest), - requestOptions: { - ...registerApiBaseOptions.requestOptions, - headers: { - ...registerApiBaseOptions.requestOptions?.headers, - }, - }, - }; - const registerApiConfig = apiConfigConstructor( - registerApiOptions, - ); - const registerResponse = await apiFetch( - registerApiConfig, - ); - - return registerResponse; - } diff --git a/server/web/src/utils/api/users.ts b/server/web/src/utils/api/users.ts new file mode 100644 index 000000000..5260cf1b0 --- /dev/null +++ b/server/web/src/utils/api/users.ts @@ -0,0 +1,109 @@ +import { + ApiOptions, + EndpointsEnum, + apiConfigConstructor, + apiFetch, + baseHeaders, + defaultApiServer, +} from '@/utils/api'; + +// Login Endpoint + +const loginApiBaseOptions: ApiOptions = { + endpointServer: defaultApiServer, + endpointPath: EndpointsEnum.login, + endpointValue: '', + requestOptions: { + method: 'POST', + headers: baseHeaders, + }, +}; + +export type PostLoginProps = { + email: string; + password: string; +}; + +export async function postLogin({ + email, + password, +}: PostLoginProps): Promise { + const loginRequest: LoginRequest = { + email: email, + password: password, + }; + const loginApiOptions: ApiOptions = { + ...loginApiBaseOptions, + body: JSON.stringify(loginRequest), + requestOptions: { + ...loginApiBaseOptions.requestOptions, + headers: { + ...loginApiBaseOptions.requestOptions?.headers, + }, + }, + }; + const loginApiConfig = apiConfigConstructor( + loginApiOptions, + ); + const loginResponse = await apiFetch( + loginApiConfig, + ); + + if (loginResponse.data == null) { + throw new Error('Login response data is null'); + } + + return loginResponse.data!; +} + +// Register Endpoint + +const registerApiBaseOptions: ApiOptions = { + endpointServer: defaultApiServer, + endpointPath: EndpointsEnum.register, + endpointValue: '', + requestOptions: { + method: 'POST', + headers: baseHeaders, + }, +}; + +export type PostRegisterProps = { + name: string; + email: string; + password: string; +}; + +export async function postRegister({ + name, + email, + password, +}: PostRegisterProps): Promise { + const registerRequest: RegisterRequest = { + name: name, + email: email, + password: password, + }; + const registerApiOptions: ApiOptions = { + ...registerApiBaseOptions, + body: JSON.stringify(registerRequest), + requestOptions: { + ...registerApiBaseOptions.requestOptions, + headers: { + ...registerApiBaseOptions.requestOptions?.headers, + }, + }, + }; + const registerApiConfig = apiConfigConstructor( + registerApiOptions, + ); + const registerResponse = await apiFetch( + registerApiConfig, + ); + + if (registerResponse.data == null) { + throw new Error('Register response data is null'); + } + + return registerResponse.data!; +} diff --git a/server/web/src/utils/models.ts b/server/web/src/utils/models.ts index 4ad9e2183..c1d8f967c 100644 --- a/server/web/src/utils/models.ts +++ b/server/web/src/utils/models.ts @@ -12,3 +12,13 @@ type LoginRequest = { type LoginResponse = { authToken: string; } + +type OrganizationRequest = { + name: string; +} + +type OrganizationResponse = { + id: number; + name: string; + users: number; +}