From 70a0c2d7b2d0ec07b9e110060ae8fc2d5c2d6715 Mon Sep 17 00:00:00 2001 From: Hoan Luu Huu <110280845+xquanluu@users.noreply.github.com> Date: Thu, 8 May 2025 19:41:05 +0700 Subject: [PATCH] support applicatin env vars (#509) * support applicatin env vars * wip * wip * wip * wip * wip --- .env | 2 +- src/api/constants.ts | 1 + src/api/index.ts | 7 + src/api/types.ts | 12 ++ .../internal/views/applications/form.tsx | 146 +++++++++++++++++- 5 files changed, 165 insertions(+), 3 deletions(-) diff --git a/.env b/.env index 92a45ae..08d4da9 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ -# VITE_API_BASE_URL=http://127.0.0.1:3000/v1 +#VITE_API_BASE_URL=http://127.0.0.1:3000/v1 #VITE_DEV_BASE_URL=http://127.0.0.1:3000/v1 ## enables choosing units and lisenced account call limits diff --git a/src/api/constants.ts b/src/api/constants.ts index ce5d433..f10fcac 100644 --- a/src/api/constants.ts +++ b/src/api/constants.ts @@ -447,3 +447,4 @@ export const API_SUBSCRIPTIONS = `${API_BASE_URL}/Subscriptions`; export const API_CHANGE_PASSWORD = `${API_BASE_URL}/change-password`; export const API_SIGNIN = `${API_BASE_URL}/signin`; export const API_GOOGLE_CUSTOM_VOICES = `${API_BASE_URL}/GoogleCustomVoices`; +export const API_APP_ENV = `${API_BASE_URL}/AppEnv`; diff --git a/src/api/index.ts b/src/api/index.ts index 5ca3c06..bfb3b78 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -34,6 +34,7 @@ import { API_CHANGE_PASSWORD, API_SIGNIN, API_GOOGLE_CUSTOM_VOICES, + API_APP_ENV, } from "./constants"; import { ROUTE_LOGIN } from "src/router/routes"; import { @@ -94,6 +95,7 @@ import type { GoogleCustomVoice, GoogleCustomVoicesQuery, SpeechSupportedLanguagesAndVoices, + AppEnv, } from "./types"; import { Availability, StatusCodes } from "./types"; import { JaegerRoot } from "./jaeger-types"; @@ -812,6 +814,11 @@ export const getGoogleCustomVoices = ( const qryStr = getQuery>(query); return getFetch(`${API_GOOGLE_CUSTOM_VOICES}?${qryStr}`); }; +// ENV VARS + +export const getAppEnvSchema = (url: string) => { + return getFetch(`${API_APP_ENV}?url=${url}`); +}; /** Wrappers for APIs that can have a mock dev server response */ diff --git a/src/api/types.ts b/src/api/types.ts index ef5a9fc..ad9650d 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -338,6 +338,7 @@ export interface Application { fallback_speech_recognizer_vendor: null | string; fallback_speech_recognizer_language: null | string; fallback_speech_recognizer_label: null | string; + env_vars: null | string; } export interface PhoneNumber { @@ -781,3 +782,14 @@ export interface CartesiaOptions { speed: number; emotion: CartesiaEmotions; } + +export interface AppEnvProperty { + description: string; + type: string; + required?: boolean; + default?: string | number | boolean; +} + +export interface AppEnv { + [key: string]: AppEnvProperty; +} diff --git a/src/containers/internal/views/applications/form.tsx b/src/containers/internal/views/applications/form.tsx index 22496e0..934a922 100644 --- a/src/containers/internal/views/applications/form.tsx +++ b/src/containers/internal/views/applications/form.tsx @@ -1,9 +1,9 @@ -import React, { useEffect, useMemo, useState } from "react"; +import React, { useEffect, useMemo, useRef, useState } from "react"; import { Button, ButtonGroup, MS } from "@jambonz/ui-kit"; import { Link, useNavigate } from "react-router-dom"; import { toastError, toastSuccess, useSelectState } from "src/store"; -import { ClipBoard, Section } from "src/components"; +import { ClipBoard, Section, Tooltip } from "src/components"; import { Selector, Checkzone, @@ -23,6 +23,7 @@ import { putApplication, useServiceProviderData, useApiData, + getAppEnvSchema, } from "src/api"; import { ROUTE_INTERNAL_ACCOUNTS, @@ -48,6 +49,7 @@ import type { WebhookMethod, UseApiDataMap, SpeechCredential, + AppEnv, } from "src/api/types"; import { MSG_REQUIRED_FIELDS, MSG_WEBHOOK_FIELDS } from "src/constants"; import { hasLength, isUserAccountScope, useRedirect } from "src/utils"; @@ -122,6 +124,12 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => { useState(""); const [initalCheckFallbackSpeech, setInitalCheckFallbackSpeech] = useState(false); + const [appEnv, setAppEnv] = useState(null); + const appEnvTimeoutRef = useRef(null); + const [envVars, setEnvVars] = useState | null>(null); /** This lets us map and render the same UI for each... */ const webhooks = [ @@ -134,6 +142,7 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => { tmpStateSet: setTmpCallWebhook, initialCheck: initialCallWebhook, required: true, + webhookEnv: appEnv, }, { label: "Call status", @@ -197,6 +206,15 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => { speech_recognizer_label: recogLabel || null, record_all_calls: recordAllCalls ? 1 : 0, use_for_fallback_speech: useForFallbackSpeech ? 1 : 0, + env_vars: envVars + ? JSON.stringify( + Object.keys(envVars).reduce( + (acc, key) => + appEnv && appEnv[key] ? { ...acc, [key]: envVars[key] } : acc, + {}, + ), + ) + : null, fallback_speech_synthesis_vendor: useForFallbackSpeech ? fallbackSpeechSynthsisVendor || null : null, @@ -513,6 +531,9 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => { application.data.fallback_speech_synthesis_voice, ); } + if (application.data.env_vars) { + setEnvVars(JSON.parse(application.data.env_vars)); + } } }, [application]); @@ -548,6 +569,33 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => { setFallbackSpeechRecognizerLabel(tmp); }; + useEffect(() => { + if (callWebhook && callWebhook.url) { + // Clear any existing timeout to prevent multiple requests + if (appEnvTimeoutRef.current) { + clearTimeout(appEnvTimeoutRef.current); + appEnvTimeoutRef.current = null; + } + + appEnvTimeoutRef.current = setTimeout(() => { + getAppEnvSchema(callWebhook.url) + .then(({ json }) => { + setAppEnv(json); + }) + .catch((error) => { + setMessage(error.msg); + }); + }, 500); + } + + return () => { + if (appEnvTimeoutRef.current) { + clearTimeout(appEnvTimeoutRef.current); + appEnvTimeoutRef.current = null; + } + }; + }, [callWebhook]); + return (
{ }} /> + + {webhook.webhookEnv && + Object.keys(webhook.webhookEnv).length > 0 && ( + <> + {Object.keys(webhook.webhookEnv).map((key) => { + const envType = webhook.webhookEnv![key].type; + const isBoolean = envType === "boolean"; + const isNumber = envType === "number"; + const defaultValue = webhook.webhookEnv![key].default; + + return ( +
+ {isBoolean ? ( + // Boolean input as checkbox + + ) : ( + // Text or number input + <> + + { + // Convert to proper type based on schema + let newValue; + if (isNumber) { + newValue = + e.target.value === "" + ? "" + : Number(e.target.value); + } else { + newValue = e.target.value; + } + + setEnvVars({ + ...(envVars || {}), + [key]: newValue, + }); + }} + /> + + )} +
+ ); + })} + + )} ); })}