From abc72a3fab354735e0946453736fd427e798f32f Mon Sep 17 00:00:00 2001 From: Barry Brands Date: Thu, 11 Apr 2024 17:17:31 +0200 Subject: [PATCH 1/9] Pages, css and services for databases --- pwa/src/apiService/apiService.ts | 5 + pwa/src/apiService/resources/database.ts | 58 ++++++ pwa/src/context/isLoading.ts | 1 + pwa/src/context/tabs.ts | 2 + pwa/src/hooks/database.ts | 68 +++++++ .../settings/databases/DatabasesPage.tsx | 12 ++ .../[databaseId]/DatabaseDetailPage.tsx | 18 ++ .../settings/databases/[databaseId]/index.tsx | 3 + pwa/src/pages/settings/databases/index.tsx | 3 + .../DatabasesTemplate.module.css | 3 + .../databasesTemplate/DatabasesTemplate.tsx | 110 +++++++++++ .../templates/settings/SettingsTemplate.tsx | 7 +- .../CreateDatabaseTemplate.module.css | 8 + .../databaseForm/CreateDatabaseTemplate.tsx | 20 ++ .../DatabaseFormTemplate.module.css | 49 +++++ .../databaseForm/DatabaseFormTemplate.tsx | 172 ++++++++++++++++++ .../EditDatabaseTemplate.module.css | 8 + .../databaseForm/EditDatabaseTemplate.tsx | 70 +++++++ 18 files changed, 616 insertions(+), 1 deletion(-) create mode 100644 pwa/src/apiService/resources/database.ts create mode 100644 pwa/src/hooks/database.ts create mode 100644 pwa/src/pages/settings/databases/DatabasesPage.tsx create mode 100644 pwa/src/pages/settings/databases/[databaseId]/DatabaseDetailPage.tsx create mode 100644 pwa/src/pages/settings/databases/[databaseId]/index.tsx create mode 100644 pwa/src/pages/settings/databases/index.tsx create mode 100644 pwa/src/templates/databasesTemplate/DatabasesTemplate.module.css create mode 100644 pwa/src/templates/databasesTemplate/DatabasesTemplate.tsx create mode 100644 pwa/src/templates/templateParts/databaseForm/CreateDatabaseTemplate.module.css create mode 100644 pwa/src/templates/templateParts/databaseForm/CreateDatabaseTemplate.tsx create mode 100644 pwa/src/templates/templateParts/databaseForm/DatabaseFormTemplate.module.css create mode 100644 pwa/src/templates/templateParts/databaseForm/DatabaseFormTemplate.tsx create mode 100644 pwa/src/templates/templateParts/databaseForm/EditDatabaseTemplate.module.css create mode 100644 pwa/src/templates/templateParts/databaseForm/EditDatabaseTemplate.tsx diff --git a/pwa/src/apiService/apiService.ts b/pwa/src/apiService/apiService.ts index be07aa82..74e82f79 100644 --- a/pwa/src/apiService/apiService.ts +++ b/pwa/src/apiService/apiService.ts @@ -23,6 +23,7 @@ import Synchroniation from "./resources/synchronization"; import Application from "./resources/application"; import Organization from "./resources/organization"; import User from "./resources/user"; +import Database from "./resources/database"; import Authentication from "./resources/authentication"; import SecurityGroup from "./resources/securityGroup"; import Mapping from "./resources/mapping"; @@ -199,6 +200,10 @@ export default class APIService { return new User(this.BaseClient, this.Send); } + public get Database(): Database { + return new Database(this.BaseClient, this.Send); + } + public get Authentication(): Authentication { return new Authentication(this.BaseClient, this.Send); } diff --git a/pwa/src/apiService/resources/database.ts b/pwa/src/apiService/resources/database.ts new file mode 100644 index 00000000..29472ea9 --- /dev/null +++ b/pwa/src/apiService/resources/database.ts @@ -0,0 +1,58 @@ +import { AxiosInstance } from "axios"; +import { TSendFunction } from "../apiService"; + +export default class Database { + private _instance: AxiosInstance; + private _send: TSendFunction; + + constructor(instance: AxiosInstance, send: TSendFunction) { + this._instance = instance; + this._send = send; + } + + public getAll = async (): Promise => { + const { data } = await this._send(this._instance, "GET", "/admin/databases"); + + return data; + }; + + public getAllSelectOptions = async (): Promise => { + const { data } = await this._send(this._instance, "GET", "/admin/databases?limit=200"); + + return data?.map((database: any) => ({ label: database.name, value: database.id })); + }; + + public getOne = async (id: string): Promise => { + const { data } = await this._send(this._instance, "GET", `/admin/databases/${id}`); + + return data; + }; + + public delete = async (variables: { id: string }): Promise => { + const { id } = variables; + + const { data } = await this._send(this._instance, "DELETE", `/admin/databases/${id}`, undefined, { + loading: "Removing database...", + success: "Database successfully removed.", + }); + return data; + }; + + public createOrUpdate = async (variables: { payload: any; id?: string }): Promise => { + const { payload, id } = variables; + + if (id) { + const { data } = await this._send(this._instance, "PUT", `/admin/databases/${id}`, payload, { + loading: "Updating database...", + success: "Database successfully updated.", + }); + return data; + } + + const { data } = await this._send(this._instance, "POST", "/admin/databases", payload, { + loading: "Creating database...", + success: "Database successfully created.", + }); + return data; + }; +} diff --git a/pwa/src/context/isLoading.ts b/pwa/src/context/isLoading.ts index b608a49c..5d47be08 100644 --- a/pwa/src/context/isLoading.ts +++ b/pwa/src/context/isLoading.ts @@ -16,6 +16,7 @@ export interface IIsLoadingContext { mappingForm?: boolean; loginForm?: boolean; templateForm?: boolean; + databaseForm?: boolean; } export const defaultIsLoadingContext: IIsLoadingContext = {}; diff --git a/pwa/src/context/tabs.ts b/pwa/src/context/tabs.ts index 0ba5d307..6c803051 100644 --- a/pwa/src/context/tabs.ts +++ b/pwa/src/context/tabs.ts @@ -9,6 +9,7 @@ export interface ITabsContext { userDetailTabs: number; organizationDetailTabs: number; mappingDetailTabs: number; + databaseDetailTabs: number; } export const defaultTabsContext = { @@ -19,6 +20,7 @@ export const defaultTabsContext = { userDetailTabs: 0, organizationDetailTabs: 0, mappingDetailTabs: 0, + databaseDetailTabs: 0, } as ITabsContext; export const useCurrentTabContext = () => { diff --git a/pwa/src/hooks/database.ts b/pwa/src/hooks/database.ts new file mode 100644 index 00000000..35c7856b --- /dev/null +++ b/pwa/src/hooks/database.ts @@ -0,0 +1,68 @@ +import * as React from "react"; +import { QueryClient, useMutation, useQuery } from "react-query"; +import APIService from "../apiService/apiService"; +import APIContext from "../apiService/apiContext"; +import { addItem, deleteItem, updateItem } from "../services/mutateQueries"; +import { navigate } from "gatsby"; +import { useDeletedItemsContext } from "../context/deletedItems"; + +export const useDatabase = (queryClient: QueryClient) => { + const API: APIService | null = React.useContext(APIContext); + const { isDeleted, addDeletedItem, removeDeletedItem } = useDeletedItemsContext(); + + const getAll = () => + useQuery("databases", API.Database.getAll, { + onError: (error) => { + console.warn(error.message); + }, + }); + + const getAllSelectOptions = () => + useQuery("database_select_options", API.Database.getAllSelectOptions, { + onError: (error) => { + console.warn(error.message); + }, + }); + + const getOne = (databaseId: string) => + useQuery(["databases", databaseId], () => API?.Database.getOne(databaseId), { + initialData: () => queryClient.getQueryData("databases")?.find((_database) => _database.id === databaseId), + onError: (error) => { + console.warn(error.message); + }, + enabled: !!databaseId && !isDeleted(databaseId), + }); + + const remove = () => + useMutation(API.Database.delete, { + onMutate: ({ id }) => addDeletedItem(id), + onSuccess: async (_, variables) => { + deleteItem(queryClient, "database", variables.id); + navigate("/settings/databases"); + }, + onError: (error, { id }) => { + removeDeletedItem(id); + console.warn(error.message); + }, + }); + + const createOrEdit = (databaseId?: string) => + useMutation(API.Database.createOrUpdate, { + onSuccess: async (newDatabase) => { + if (databaseId) { + updateItem(queryClient, "databases", newDatabase); + navigate("/settings"); + } + + if (!databaseId) { + addItem(queryClient, "databases", newDatabase); + navigate(`/settings/databases/${newDatabase.id}`); + } + }, + onError: (error) => { + console.warn(error.message); + }, + }); + + return { getAll, getAllSelectOptions, getOne, createOrEdit, remove }; +}; diff --git a/pwa/src/pages/settings/databases/DatabasesPage.tsx b/pwa/src/pages/settings/databases/DatabasesPage.tsx new file mode 100644 index 00000000..57c35bc9 --- /dev/null +++ b/pwa/src/pages/settings/databases/DatabasesPage.tsx @@ -0,0 +1,12 @@ +import * as React from "react"; +import { navigate } from "gatsby"; + +const DatabasesPage: React.FC = () => { + React.useEffect(() => { + navigate("/settings"); + }); + + return <>; +}; + +export default DatabasesPage; diff --git a/pwa/src/pages/settings/databases/[databaseId]/DatabaseDetailPage.tsx b/pwa/src/pages/settings/databases/[databaseId]/DatabaseDetailPage.tsx new file mode 100644 index 00000000..108c3eb1 --- /dev/null +++ b/pwa/src/pages/settings/databases/[databaseId]/DatabaseDetailPage.tsx @@ -0,0 +1,18 @@ +import * as React from "react"; +import { PageProps } from "gatsby"; +import { DashboardTemplate } from "../../../../templates/dashboard/DashboardTemplate"; +import { CreateDatabaseTemplate } from "../../../../templates/templateParts/databaseForm/CreateDatabaseTemplate"; +import { EditDatabaseTemplate } from "../../../../templates/templateParts/databaseForm/EditDatabaseTemplate"; + +const DatabaseDetailPage: React.FC = (props: PageProps) => { + const databaseId = props.params.databaseId === "new" ? null : props.params.databaseId; + + return ( + + {!databaseId && } + {databaseId && } + + ); +}; + +export default DatabaseDetailPage; diff --git a/pwa/src/pages/settings/databases/[databaseId]/index.tsx b/pwa/src/pages/settings/databases/[databaseId]/index.tsx new file mode 100644 index 00000000..79b85f56 --- /dev/null +++ b/pwa/src/pages/settings/databases/[databaseId]/index.tsx @@ -0,0 +1,3 @@ +import DatabaseDetailPage from "./DatabaseDetailPage"; + +export default DatabaseDetailPage; diff --git a/pwa/src/pages/settings/databases/index.tsx b/pwa/src/pages/settings/databases/index.tsx new file mode 100644 index 00000000..496cc984 --- /dev/null +++ b/pwa/src/pages/settings/databases/index.tsx @@ -0,0 +1,3 @@ +import DatabasesPage from "./DatabasesPage"; + +export default DatabasesPage; diff --git a/pwa/src/templates/databasesTemplate/DatabasesTemplate.module.css b/pwa/src/templates/databasesTemplate/DatabasesTemplate.module.css new file mode 100644 index 00000000..32233480 --- /dev/null +++ b/pwa/src/templates/databasesTemplate/DatabasesTemplate.module.css @@ -0,0 +1,3 @@ +.container > *:not(:last-child) { + margin-block-end: var(--gateway-ui-size-2xl); +} diff --git a/pwa/src/templates/databasesTemplate/DatabasesTemplate.tsx b/pwa/src/templates/databasesTemplate/DatabasesTemplate.tsx new file mode 100644 index 00000000..4476164a --- /dev/null +++ b/pwa/src/templates/databasesTemplate/DatabasesTemplate.tsx @@ -0,0 +1,110 @@ +import * as React from "react"; +import * as styles from "./DatabasesTemplate.module.css"; +import { Link } from "@gemeente-denhaag/components-react"; +import { useTranslation } from "react-i18next"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@gemeente-denhaag/table"; +import { navigate } from "gatsby"; +import { Container } from "@conduction/components"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faArrowRight, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { useQueryClient } from "react-query"; +import { translateDate } from "../../services/dateFormat"; +import Skeleton from "react-loading-skeleton"; +import { useDatabase } from "../../hooks/database"; +import { Button } from "../../components/button/Button"; +import { OverviewPageHeaderTemplate } from "../templateParts/overviewPageHeader/OverviewPageHeaderTemplate"; +import { useBulkSelect } from "../../hooks/useBulkSelect"; +import { BulkActionButton } from "../../components/bulkActionButton/BulkActionButton"; + +export const DatabasesTemplate: React.FC = () => { + const { t, i18n } = useTranslation(); + + const queryClient = useQueryClient(); + const _useDatabases = useDatabase(queryClient); + const getDatabases = _useDatabases.getAll(); + const deleteDatabase = _useDatabases.remove(); + + const { CheckboxBulkSelectAll, CheckboxBulkSelectOne, selectedItems, toggleItem } = useBulkSelect(getDatabases.data); + + const handleBulkDelete = (): void => { + selectedItems.forEach((item) => deleteDatabase.mutate({ id: item })); + }; + + return ( + + navigate(`/settings/databases/new`)} + /> + } + /> + + {getDatabases.isSuccess && ( +
+ + + + + + + + + {t("Name")} + {t("Organizations")} + {t("Date created")} + {t("Date modified")} + + + + + {getDatabases.data.map((database: any) => ( + toggleItem(database.id)}> + {} + + {database.name} + + + {database.organizations.length > 1 + ? `${database.organizations.length} organizations` + : database.organizations[0]?.name} + + + {translateDate(i18n.language, database.dateCreated) ?? "-"} + + {translateDate(i18n.language, database.dateModified) ?? "-"} + + navigate(`/settings/databases/${database.id}`)}> + } iconAlign="start"> + {t("Details")} + + + + ))} + {!getDatabases.data.length && ( + + No databases found + + + + + + + )} + +
+
+ )} + + {getDatabases.isLoading && } +
+ ); +}; diff --git a/pwa/src/templates/settings/SettingsTemplate.tsx b/pwa/src/templates/settings/SettingsTemplate.tsx index 236df0a2..8bce5d10 100644 --- a/pwa/src/templates/settings/SettingsTemplate.tsx +++ b/pwa/src/templates/settings/SettingsTemplate.tsx @@ -9,6 +9,7 @@ import { useCurrentTabContext } from "../../context/tabs"; import { ApplicationsTemplate } from "../applicationsTemplate/ApplicationsTemplate"; import { OrganizationsTemplate } from "../organizationsTemplate/OrganizationsTemplate"; import { UsersTemplate } from "../usersTemplate/UsersTemplate"; +import { DatabasesTemplate } from "../databasesTemplate/DatabasesTemplate"; import { AuthenticationsTemplate } from "../authenticationsTemplate/AuthenticationsTemplate"; export const SettingsTemplate: React.FC = () => { @@ -33,7 +34,8 @@ export const SettingsTemplate: React.FC = () => { - + + @@ -52,6 +54,9 @@ export const SettingsTemplate: React.FC = () => { + + + diff --git a/pwa/src/templates/templateParts/databaseForm/CreateDatabaseTemplate.module.css b/pwa/src/templates/templateParts/databaseForm/CreateDatabaseTemplate.module.css new file mode 100644 index 00000000..0d0df47e --- /dev/null +++ b/pwa/src/templates/templateParts/databaseForm/CreateDatabaseTemplate.module.css @@ -0,0 +1,8 @@ +.container { + box-sizing: border-box; + background-color: var(--gateway-ui-color-white); + padding-inline-start: var(--gateway-ui-size-md); + padding-inline-end: var(--gateway-ui-size-md); + padding-block-start: var(--gateway-ui-size-md); + padding-block-end: var(--gateway-ui-size-md); +} diff --git a/pwa/src/templates/templateParts/databaseForm/CreateDatabaseTemplate.tsx b/pwa/src/templates/templateParts/databaseForm/CreateDatabaseTemplate.tsx new file mode 100644 index 00000000..175520ca --- /dev/null +++ b/pwa/src/templates/templateParts/databaseForm/CreateDatabaseTemplate.tsx @@ -0,0 +1,20 @@ +import * as React from "react"; +import * as styles from "./CreateDatabaseTemplate.module.css"; +import { formId, DatabaseFormTemplate } from "./DatabaseFormTemplate"; +import { useTranslation } from "react-i18next"; +import { Container } from "@conduction/components"; +import { useIsLoadingContext } from "../../../context/isLoading"; +import { FormHeaderTemplate } from "../formHeader/FormHeaderTemplate"; + +export const CreateDatabaseTemplate: React.FC = () => { + const { t } = useTranslation(); + const { isLoading } = useIsLoadingContext(); + + return ( + + + + + + ); +}; diff --git a/pwa/src/templates/templateParts/databaseForm/DatabaseFormTemplate.module.css b/pwa/src/templates/templateParts/databaseForm/DatabaseFormTemplate.module.css new file mode 100644 index 00000000..b2cdd66b --- /dev/null +++ b/pwa/src/templates/templateParts/databaseForm/DatabaseFormTemplate.module.css @@ -0,0 +1,49 @@ +.container > :not(:last-child) { + margin-block-end: var(--gateway-ui-size-2xl); +} + +.formContainer { + background-color: var(--gateway-ui-color-white); + padding-inline-start: var(--gateway-ui-size-md); + padding-inline-end: var(--gateway-ui-size-md); + padding-block-start: var(--gateway-ui-size-md); + padding-block-end: var(--gateway-ui-size-md); +} + +.gridContainer > *:not(:first-child) { + margin-block-start: var(--gateway-ui-size-sm); +} + +.grid { + display: grid; + grid-gap: var(--gateway-ui-size-lg); + grid-template-columns: 1fr 1fr; +} + +.deleteButton { + --denhaag-button-primary-action-background-color: var(--gateway-ui-color-red); +} + +.deleteButton:hover { + --denhaag-button-primary-action-hover-background-color: var( + --gateway-ui-color-dark-red + ); +} + +.tabContainer { + background: var(--gateway-ui-color-white); + margin-block-start: var(--gateway-ui-size-2xl); + padding-block: var(--gateway-ui-size-md); + padding-inline: var(--gateway-ui-size-md); +} + +.tabPanel { + padding-inline-start: var(--gateway-ui-size-lg) !important; + padding-inline-end: var(--gateway-ui-size-lg) !important; + padding-block-start: var(--gateway-ui-size-lg) !important; + padding-block-end: var(--gateway-ui-size-lg) !important; +} + +.tab { + max-width: unset !important; +} diff --git a/pwa/src/templates/templateParts/databaseForm/DatabaseFormTemplate.tsx b/pwa/src/templates/templateParts/databaseForm/DatabaseFormTemplate.tsx new file mode 100644 index 00000000..16de53c4 --- /dev/null +++ b/pwa/src/templates/templateParts/databaseForm/DatabaseFormTemplate.tsx @@ -0,0 +1,172 @@ +import * as React from "react"; +import * as styles from "./DatabaseFormTemplate.module.css"; +import FormField, { FormFieldInput, FormFieldLabel } from "@gemeente-denhaag/form-field"; +import { useTranslation } from "react-i18next"; +import { InputPassword, InputText, SelectMultiple, SelectSingle, Textarea } from "@conduction/components"; +import { useForm } from "react-hook-form"; +import { useQueryClient } from "react-query"; +import { useDatabase } from "../../../hooks/database"; +import Skeleton from "react-loading-skeleton"; +import { validatePassword } from "../../../services/stringValidations"; +import { useOrganization } from "../../../hooks/organization"; +import { useIsLoadingContext } from "../../../context/isLoading"; +import { useApplication } from "../../../hooks/application"; +import { IKeyValue } from "@conduction/components/lib/components/formFields"; +import { useSecurityGroup } from "../../../hooks/securityGroup"; +import { enrichValidation } from "../../../services/enrichReactHookFormValidation"; + +interface DatabaseFormTemplateProps { + database?: any; +} + +export const formId: string = "database-form"; + +export const DatabaseFormTemplate: React.FC = ({ database }) => { + const { t } = useTranslation(); + const { setIsLoading, isLoading } = useIsLoadingContext(); + + const queryClient = useQueryClient(); + const createOrEditDatabase = useDatabase(queryClient).createOrEdit(database?.id); + + const getOrganization = useOrganization(queryClient).getAll(); + + const organizationOptions = getOrganization.data?.map((_organization: any) => ({ + label: _organization.name, + value: _organization.id, + })); + + const _useOrganizations = useOrganization(queryClient); + const getOrganizations = _useOrganizations.getAll(); + + const { + register, + handleSubmit, + setValue, + formState: { errors, isSubmitted }, + control, + watch, + trigger, + } = useForm(); + + const handleSetFormValues = (database: any): void => { + const basicFields: string[] = ["reference", "name", "description", "version"]; + basicFields.forEach((field) => setValue(field, database[field])); + + setValue( + "organizations", + database.organizations.map((organization: any) => ({ label: organization.name, value: organization.id })), + ); + }; + + const onSubmit = (data: any): void => { + const payload = { + ...data, + organizations: + data.organizations && + data.organizations.map((organization: any) => `/admin/organizations/${organization.value}`), + }; + + data.uri === "" && delete payload.uri; + + createOrEditDatabase.mutate({ payload, id: database?.id }); + database?.id && queryClient.setQueryData(["databases", database.id], data); + }; + + React.useEffect(() => { + setIsLoading({ databaseForm: createOrEditDatabase.isLoading }); + }, [createOrEditDatabase.isLoading]); + + React.useEffect(() => { + database && handleSetFormValues(database); + }, [database]); + + return ( +
+
+
+ + + {t("Name")} + + + + + + {t("Reference")} + + + + + + + {t("Description")} +