From aa05fe1704b67ffbe1e690b4dbc7854174864a4d Mon Sep 17 00:00:00 2001 From: Mohamed Marrouchi Date: Tue, 17 Sep 2024 17:12:20 +0100 Subject: [PATCH 1/3] fix: load config on runtime --- docker/docker-compose.dev.yml | 5 +++ docker/docker-compose.yml | 8 +---- frontend/Dockerfile | 12 ------- frontend/next.config.mjs | 8 +++++ frontend/src/hooks/useConfig.tsx | 50 ++++++++++++++++++++++++++++ frontend/src/pages/_app.tsx | 57 +++++++++++++++++--------------- frontend/src/pages/api/config.ts | 16 +++++++++ 7 files changed, 110 insertions(+), 46 deletions(-) create mode 100644 frontend/src/hooks/useConfig.tsx create mode 100644 frontend/src/pages/api/config.ts diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 3ba63cc2..b4171110 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -16,6 +16,11 @@ services: - ../api/migrations:/app/migrations #- ../api/node_modules:/app/node_modules command: ["npm", "run", "start:debug"] + + hexabot-frontend: + build: + context: ../ + dockerfile: ./frontend/Dockerfile mongo-express: container_name: mongoUi diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 80102c66..5549acf3 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -30,7 +30,6 @@ services: condition: service_healthy database-init: condition: service_completed_successfully - healthcheck: test: "wget --spider http://localhost:3000" interval: 10s @@ -40,12 +39,7 @@ services: hexabot-frontend: container_name: frontend - build: - context: ../ - dockerfile: ./frontend/Dockerfile - args: - - NEXT_PUBLIC_API_ORIGIN=${NEXT_PUBLIC_API_ORIGIN} - - NEXT_PUBLIC_SSO_ENABLED=${NEXT_PUBLIC_SSO_ENABLED} + image: hexabot-ui:latest env_file: .env ports: - ${APP_FRONTEND_PORT}:8080 diff --git a/frontend/Dockerfile b/frontend/Dockerfile index cea1c11b..d8d812c6 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -21,14 +21,6 @@ RUN \ # Rebuild the source code only when needed FROM base AS builder -ARG NEXT_PUBLIC_API_ORIGIN -ENV NEXT_PUBLIC_API_ORIGIN=${NEXT_PUBLIC_API_ORIGIN} -ARG NEXT_PUBLIC_SSO_ENABLED -ENV NEXT_PUBLIC_SSO_ENABLED=${NEXT_PUBLIC_SSO_ENABLED} - -ENV REACT_APP_WIDGET_API_URL=${NEXT_PUBLIC_API_ORIGIN} -ENV REACT_APP_WIDGET_CHANNEL=test -ENV REACT_APP_WIDGET_TOKEN=test WORKDIR /app @@ -57,10 +49,6 @@ ENV NODE_ENV production # Uncomment the following line in case you want to disable telemetry during runtime. ENV NEXT_TELEMETRY_DISABLED 1 -# Set the environment variable API_ORIGIN -ENV NEXT_PUBLIC_API_ORIGIN ${NEXT_PUBLIC_API_ORIGIN:-"http://localhost:3000"} -ENV NEXT_PUBLIC_SSO_ENABLED ${NEXT_PUBLIC_SSO_ENABLED:-"false"} - RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs index 774cf83f..f1c7e508 100644 --- a/frontend/next.config.mjs +++ b/frontend/next.config.mjs @@ -4,6 +4,14 @@ import withTM from "next-transpile-modules"; const apiUrl = process.env.NEXT_PUBLIC_API_ORIGIN || "http://localhost:4000/"; const url = new URL(apiUrl); const nextConfig = withTM(["hexabot-widget"])({ + async rewrites() { + return [ + { + source: "/config", + destination: "/api/config", + }, + ]; + }, webpack(config, _options) { return config; }, diff --git a/frontend/src/hooks/useConfig.tsx b/frontend/src/hooks/useConfig.tsx new file mode 100644 index 00000000..f8a4fd94 --- /dev/null +++ b/frontend/src/hooks/useConfig.tsx @@ -0,0 +1,50 @@ +import { createContext, useContext, useEffect, useState } from "react"; + +const ConfigContext = createContext(null); + +export interface IConfig { + NEXT_PUBLIC_API_ORIGIN: string; + NEXT_PUBLIC_SSO_ENABLED: boolean; + REACT_APP_WIDGET_API_URL: string; + REACT_APP_WIDGET_CHANNEL: string; + REACT_APP_WIDGET_TOKEN: string; +} + +export const ConfigProvider = ({ children }) => { + const [config, setConfig] = useState(null); + + useEffect(() => { + const fetchConfig = async () => { + try { + const res = await fetch("/config"); + const data = await res.json(); + + setConfig(data); + } catch (error) { + // eslint-disable-next-line no-console + console.error("Failed to fetch configuration:", error); + } + }; + + fetchConfig(); + }, []); + + if (!config) { + // You can return a loader here if you want + return null; + } + + return ( + {children} + ); +}; + +export const useConfig = () => { + const context = useContext(ConfigContext); + + if (!context) { + throw new Error("useConfig must be used within a ConfigProvider"); + } + + return context; +}; diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx index ad0bbab1..b93e392d 100644 --- a/frontend/src/pages/_app.tsx +++ b/frontend/src/pages/_app.tsx @@ -20,6 +20,7 @@ import { ReactQueryDevtools } from "react-query/devtools"; import { SnackbarCloseButton } from "@/app-components/displays/Toast/CloseButton"; import { ApiClientProvider } from "@/hooks/useApiClient"; import { AuthProvider } from "@/hooks/useAuth"; +import { ConfigProvider } from "@/hooks/useConfig"; import { PermissionProvider } from "@/hooks/useHasPermission"; import { SettingsProvider } from "@/hooks/useSetting"; import { ToastProvider } from "@/hooks/useToast"; @@ -69,33 +70,35 @@ const App = ({ Component, pageProps }: TAppPropsWithLayout) => { />
- - ( - - )} - > - - - - - - - - - {getLayout()} - - - - - - - - - - + + + ( + + )} + > + + + + + + + + + {getLayout()} + + + + + + + + + + +
); diff --git a/frontend/src/pages/api/config.ts b/frontend/src/pages/api/config.ts new file mode 100644 index 00000000..2af6d1ab --- /dev/null +++ b/frontend/src/pages/api/config.ts @@ -0,0 +1,16 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +type ResponseData = { + apiUrl: string; + ssoEnabled: boolean; +}; + +export default function handler( + req: NextApiRequest, + res: NextApiResponse, +) { + res.status(200).json({ + apiUrl: process.env.NEXT_PUBLIC_API_ORIGIN || "http://localhost:3000", + ssoEnabled: process.env.NEXT_PUBLIC_SSO_ENABLED === "true" || false, + }); +} From cdf846131211e7f295f3d4bf1e1858e6ad47cc36 Mon Sep 17 00:00:00 2001 From: Yassine Sallemi Date: Fri, 20 Sep 2024 16:21:30 +0100 Subject: [PATCH 2/3] fix: config fetched on runtime --- frontend/next.config.mjs | 20 ---------------- .../tables/columns/renderPicture.tsx | 4 ++++ .../src/components/inbox/components/Chat.tsx | 10 ++++++-- .../inbox/components/ChatActions.tsx | 4 +++- .../inbox/components/ConversationsList.tsx | 3 +++ .../components/inbox/helpers/mapMessages.tsx | 12 ++++------ .../components/nlp/components/NlpSample.tsx | 6 ++--- frontend/src/components/users/index.tsx | 11 ++++----- frontend/src/hooks/useApiClient.tsx | 7 +++--- frontend/src/hooks/useConfig.tsx | 9 +++---- frontend/src/layout/Header.tsx | 9 ++++--- frontend/src/layout/VerticalMenu.tsx | 11 +++++---- frontend/src/pages/api/config.ts | 2 +- frontend/src/pages/visual-editor.tsx | 9 +++---- frontend/src/websocket/SocketIoClient.ts | 9 ++----- frontend/src/websocket/socket-hooks.tsx | 24 ++++++++++++------- 16 files changed, 70 insertions(+), 80 deletions(-) diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs index f1c7e508..6d0080df 100644 --- a/frontend/next.config.mjs +++ b/frontend/next.config.mjs @@ -1,8 +1,6 @@ /** @type {import('next').NextConfig} */ import withTM from "next-transpile-modules"; -const apiUrl = process.env.NEXT_PUBLIC_API_ORIGIN || "http://localhost:4000/"; -const url = new URL(apiUrl); const nextConfig = withTM(["hexabot-widget"])({ async rewrites() { return [ @@ -16,29 +14,11 @@ const nextConfig = withTM(["hexabot-widget"])({ return config; }, publicRuntimeConfig: { - apiUrl, - ssoEnabled: process.env.NEXT_PUBLIC_SSO_ENABLED === "true", lang: { default: "en", }, }, output: "standalone", - images: { - remotePatterns: [ - { - protocol: "https", - hostname: url.hostname, - port: url.port, - pathname: "/attachment/**", - }, - { - protocol: "http", - hostname: url.hostname, - port: url.port, - pathname: "/attachment/**", - }, - ], - }, }); export default nextConfig; diff --git a/frontend/src/app-components/tables/columns/renderPicture.tsx b/frontend/src/app-components/tables/columns/renderPicture.tsx index 33af1740..3684a00a 100644 --- a/frontend/src/app-components/tables/columns/renderPicture.tsx +++ b/frontend/src/app-components/tables/columns/renderPicture.tsx @@ -11,12 +11,15 @@ import { Grid } from "@mui/material"; import { GridRenderCellParams } from "@mui/x-data-grid"; import { getAvatarSrc } from "@/components/inbox/helpers/mapMessages"; +import { useConfig } from "@/hooks/useConfig"; import { EntityType } from "@/services/types"; export const buildRenderPicture = ( entityType: EntityType.USER | EntityType.SUBSCRIBER, ) => function RenderPicture(params: GridRenderCellParams) { + const { apiUrl } = useConfig(); + return ( @@ -118,6 +123,7 @@ export function Chat() { i18n.language, )}`} src={getAvatarSrc( + apiUrl, message.sender ? EntityType.SUBSCRIBER : EntityType.USER, diff --git a/frontend/src/components/inbox/components/ChatActions.tsx b/frontend/src/components/inbox/components/ChatActions.tsx index b91f5cc9..605ab2e4 100644 --- a/frontend/src/components/inbox/components/ChatActions.tsx +++ b/frontend/src/components/inbox/components/ChatActions.tsx @@ -18,12 +18,14 @@ import { Input } from "@/app-components/inputs/Input"; import { useFind } from "@/hooks/crud/useFind"; import { useUpdate } from "@/hooks/crud/useUpdate"; import { useAuth } from "@/hooks/useAuth"; +import { useConfig } from "@/hooks/useConfig"; import { EntityType } from "@/services/types"; import { getAvatarSrc } from "../helpers/mapMessages"; import { useChat } from "../hooks/ChatContext"; export const ChatActions = () => { + const { apiUrl } = useConfig(); const { t } = useTranslation(); const { subscriber: activeChat } = useChat(); const [takeoverBy, setTakeoverBy] = useState( @@ -57,7 +59,7 @@ export const ChatActions = () => { diff --git a/frontend/src/components/inbox/components/ConversationsList.tsx b/frontend/src/components/inbox/components/ConversationsList.tsx index b032f514..666cadd4 100644 --- a/frontend/src/components/inbox/components/ConversationsList.tsx +++ b/frontend/src/components/inbox/components/ConversationsList.tsx @@ -16,6 +16,7 @@ import InboxIcon from "@mui/icons-material/MoveToInbox"; import { Chip, debounce, Grid } from "@mui/material"; import { useTranslation } from "react-i18next"; +import { useConfig } from "@/hooks/useConfig"; import { Title } from "@/layout/content/Title"; import { EntityType } from "@/services/types"; @@ -29,6 +30,7 @@ export const SubscribersList = (props: { searchPayload: any; assignedTo: AssignedTo; }) => { + const { apiUrl } = useConfig(); const { t, i18n } = useTranslation(); const chat = useChat(); const { fetchNextPage, isFetching, subscribers, hasNextPage } = @@ -58,6 +60,7 @@ export const SubscribersList = (props: { > } diff --git a/frontend/src/components/users/index.tsx b/frontend/src/components/users/index.tsx index a77391bf..6cde7a77 100644 --- a/frontend/src/components/users/index.tsx +++ b/frontend/src/components/users/index.tsx @@ -11,7 +11,6 @@ import { faUsers } from "@fortawesome/free-solid-svg-icons"; import PersonAddAlt1Icon from "@mui/icons-material/PersonAddAlt1"; import { Button, Grid, Paper } from "@mui/material"; import { GridColDef } from "@mui/x-data-grid"; -import getConfig from "next/config"; import { useTranslation } from "react-i18next"; import { ChipEntity } from "@/app-components/displays/ChipEntity"; @@ -25,6 +24,7 @@ import { buildRenderPicture } from "@/app-components/tables/columns/renderPictur import { DataGrid } from "@/app-components/tables/DataGrid"; import { useFind } from "@/hooks/crud/useFind"; import { useUpdate } from "@/hooks/crud/useUpdate"; +import { useConfig } from "@/hooks/useConfig"; import { getDisplayDialogs, useDialog } from "@/hooks/useDialog"; import { useHasPermission } from "@/hooks/useHasPermission"; import { useSearch } from "@/hooks/useSearch"; @@ -39,9 +39,8 @@ import { getDateTimeFormatter } from "@/utils/date"; import { EditUserDialog } from "./EditUserDialog"; import { InvitationDialog } from "./InvitationDialog"; -const { publicRuntimeConfig } = getConfig(); - export const Users = () => { + const { ssoEnabled } = useConfig(); const { t } = useTranslation(); const { toast } = useToast(); const { mutateAsync: updateUser } = useUpdate(EntityType.USER, { @@ -157,7 +156,7 @@ export const Users = () => { }, }); }} - disabled={publicRuntimeConfig.ssoEnabled} + disabled={ssoEnabled} > {t(params.row.state ? "label.enabled" : "label.disabled")} @@ -188,7 +187,7 @@ export const Users = () => { valueGetter: (params) => t("datetime.updated_at", getDateTimeFormatter(params)), }, - ...(!publicRuntimeConfig.ssoEnabled ? [actionColumns] : []), + ...(!ssoEnabled ? [actionColumns] : []), ]; return ( @@ -207,7 +206,7 @@ export const Users = () => { - {!publicRuntimeConfig.ssoEnabled && + {!ssoEnabled && hasPermission(EntityType.USER, PermissionAction.CREATE) ? (