mirror of
https://github.com/jambonz/jambonz-webapp.git
synced 2025-12-19 05:37:43 +00:00
support applicatin env vars (#509)
* support applicatin env vars * wip * wip * wip * wip * wip
This commit is contained in:
2
.env
2
.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
|
||||
|
||||
@@ -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`;
|
||||
|
||||
@@ -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<Partial<GoogleCustomVoicesQuery>>(query);
|
||||
return getFetch<GoogleCustomVoice[]>(`${API_GOOGLE_CUSTOM_VOICES}?${qryStr}`);
|
||||
};
|
||||
// ENV VARS
|
||||
|
||||
export const getAppEnvSchema = (url: string) => {
|
||||
return getFetch<AppEnv>(`${API_APP_ENV}?url=${url}`);
|
||||
};
|
||||
|
||||
/** Wrappers for APIs that can have a mock dev server response */
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<AppEnv | null>(null);
|
||||
const appEnvTimeoutRef = useRef<number | null>(null);
|
||||
const [envVars, setEnvVars] = useState<Record<
|
||||
string,
|
||||
string | number | boolean
|
||||
> | 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 (
|
||||
<Section slim>
|
||||
<form
|
||||
@@ -699,6 +747,100 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
|
||||
}}
|
||||
/>
|
||||
</Checkzone>
|
||||
|
||||
{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 (
|
||||
<div className="inp" key={key}>
|
||||
{isBoolean ? (
|
||||
// Boolean input as checkbox
|
||||
<label htmlFor={`env_${key}`} className="chk">
|
||||
<input
|
||||
id={`env_${key}`}
|
||||
type="checkbox"
|
||||
name={`env_${key}`}
|
||||
required={webhook.webhookEnv![key].required}
|
||||
checked={
|
||||
envVars && envVars[key] !== undefined
|
||||
? Boolean(envVars[key])
|
||||
: Boolean(defaultValue)
|
||||
}
|
||||
onChange={(e) => {
|
||||
setEnvVars({
|
||||
...(envVars || {}),
|
||||
[key]: e.target.checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Tooltip
|
||||
text={webhook.webhookEnv![key].description}
|
||||
>
|
||||
{key}
|
||||
{webhook.webhookEnv![key].required && (
|
||||
<span>*</span>
|
||||
)}
|
||||
</Tooltip>
|
||||
</label>
|
||||
) : (
|
||||
// Text or number input
|
||||
<>
|
||||
<label htmlFor={`env_${key}`}>
|
||||
<Tooltip
|
||||
text={webhook.webhookEnv![key].description}
|
||||
>
|
||||
{key}
|
||||
{webhook.webhookEnv![key].required && (
|
||||
<span>*</span>
|
||||
)}
|
||||
</Tooltip>
|
||||
</label>
|
||||
<input
|
||||
id={`env_${key}`}
|
||||
type={isNumber ? "number" : "text"}
|
||||
name={`env_${key}`}
|
||||
placeholder={
|
||||
webhook.webhookEnv![key].description
|
||||
}
|
||||
required={webhook.webhookEnv![key].required}
|
||||
value={
|
||||
envVars && envVars[key] !== undefined
|
||||
? String(envVars[key])
|
||||
: defaultValue !== undefined
|
||||
? String(defaultValue)
|
||||
: ""
|
||||
}
|
||||
onChange={(e) => {
|
||||
// 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,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</fieldset>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user