From e0687c2b53e448e8ba3ab4732c55aac574809097 Mon Sep 17 00:00:00 2001 From: Quan HL Date: Mon, 23 Oct 2023 10:33:37 +0700 Subject: [PATCH] wip --- src/api/constants.ts | 1 + src/api/index.ts | 140 ++++++++++++++++++ src/api/types.ts | 31 ++++ src/common/types.ts | 3 +- src/components/menu/index.tsx | 60 ++++++++ src/components/password-input/index.tsx | 3 + src/storage/index.ts | 26 +++- src/window/phone/index.tsx | 112 ++++++++++---- .../settings/{advance.tsx => advanced.tsx} | 55 ++++++- src/window/settings/index.tsx | 6 +- 10 files changed, 399 insertions(+), 38 deletions(-) create mode 100644 src/api/constants.ts create mode 100644 src/api/index.ts create mode 100644 src/api/types.ts create mode 100644 src/components/menu/index.tsx rename src/window/settings/{advance.tsx => advanced.tsx} (54%) diff --git a/src/api/constants.ts b/src/api/constants.ts new file mode 100644 index 0000000..9b643dd --- /dev/null +++ b/src/api/constants.ts @@ -0,0 +1 @@ +export const MSG_SOMETHING_WRONG = "Something went wrong, please try again"; diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..533d8ad --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,140 @@ +import { Application, FetchError, FetchTransport, StatusCodes } from "./types"; +import { MSG_SOMETHING_WRONG } from "./constants"; +import { getAdvancedSettings } from "src/storage"; + +const fetchTransport = ( + url: string, + options: RequestInit +): Promise> => { + return new Promise(async (resolve, reject) => { + try { + const response = await fetch(url, options); + const transport: FetchTransport = { + headers: response.headers, + status: response.status, + json: {}, + }; + + // Redirect unauthorized + if (response.status === StatusCodes.UNAUTHORIZED) { + reject(); + } + + // API error handling returns { msg: string; } + // See @type StatusJSON and StatusEmpty in ./types + if ( + response.status >= StatusCodes.BAD_REQUEST && + response.status <= StatusCodes.INTERNAL_SERVER_ERROR + ) { + try { + const errJson = await response.json(); + reject({ + status: response.status, + ...errJson, + }); + } catch (error) { + reject({ + status: response.status, + msg: MSG_SOMETHING_WRONG, + }); + } + } + + // API success handling returns a valid JSON response + // This could either be a DTO object or a generic response + // See types for various responses in ./types + if ( + response.status === StatusCodes.OK || + response.status === StatusCodes.CREATED + ) { + // Handle blobs -- e.g. pcap file API for RecentCalls + if ( + options.headers!["Content-Type" as keyof HeadersInit] === + "application/octet-stream" + ) { + const blob: Blob = await response.blob(); + + transport.blob = blob; + } else { + const json: Type = await response.json(); + + transport.json = json; + } + } + + resolve(transport); + // TypeError "Failed to fetch" + // net::ERR_CONNECTION_REFUSED + // This is the case if the server is unreachable... + } catch (error: unknown) { + reject({ + status: StatusCodes.INTERNAL_SERVER_ERROR, + msg: (error as TypeError).message, + }); + } + }); +}; + +const getAuthHeaders = () => { + const advancedSettings = getAdvancedSettings(); + let token = advancedSettings.apiKey ?? null; + + return { + "Content-Type": "application/json", + ...(token && { Authorization: `Bearer ${token}` }), + }; +}; + +export const getFetch = (url: string) => { + return fetchTransport(url, { + headers: getAuthHeaders(), + }); +}; + +export const postFetch = ( + url: string, + payload?: Payload +) => { + return fetchTransport(url, { + method: "POST", + ...(payload && { body: JSON.stringify(payload) }), + headers: getAuthHeaders(), + }); +}; + +export const putFetch = (url: string, payload: Payload) => { + return fetchTransport(url, { + method: "PUT", + body: JSON.stringify(payload), + headers: getAuthHeaders(), + }); +}; + +export const deleteFetch = (url: string) => { + return fetchTransport(url, { + method: "DELETE", + headers: getAuthHeaders(), + }); +}; + +// GET Devices Users +export const getRegisteredUser = () => { + const advancedSettings = getAdvancedSettings(); + return getFetch( + `${advancedSettings.apiServer}/Accounts/${advancedSettings.accountSid}/RegisteredSipUsers` + ); +}; + +export const getApplications = () => { + const advancedSettings = getAdvancedSettings(); + return getFetch( + `${advancedSettings.apiServer}/Accounts/${advancedSettings.accountSid}/Applications` + ); +}; + +export const getQueues = () => { + const advancedSettings = getAdvancedSettings(); + return getFetch( + `${advancedSettings.apiServer}/Accounts/${advancedSettings.accountSid}/Queues` + ); +}; diff --git a/src/api/types.ts b/src/api/types.ts new file mode 100644 index 0000000..2524135 --- /dev/null +++ b/src/api/types.ts @@ -0,0 +1,31 @@ +export enum StatusCodes { + OK = 200, + CREATED = 201, + ACCEPTED = 202, + NO_CONTENT = 204, + BAD_REQUEST = 400, + UNAUTHORIZED = 401, + FORBIDDEN = 403, + NOT_FOUND = 404, + UNPROCESSABLE_ENTITY = 422, + INTERNAL_SERVER_ERROR = 500, + /** SMPP temporarily unavailable */ + TEMPORARILY_UNAVAILABLE = 480, +} + +export interface FetchTransport { + headers: Headers; + status: StatusCodes; + json: Type; + blob?: Blob; +} + +export interface FetchError { + status: StatusCodes; + msg: string; +} + +export interface Application { + application_sid: string; + name: string; +} diff --git a/src/common/types.ts b/src/common/types.ts index 85a54d9..d507c02 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -58,7 +58,8 @@ export interface AppSettings { } export interface AdvancedAppSettings { - apikey: string; + accountSid: string; + apiKey: string; apiServer: string; } diff --git a/src/components/menu/index.tsx b/src/components/menu/index.tsx new file mode 100644 index 0000000..baa5e14 --- /dev/null +++ b/src/components/menu/index.tsx @@ -0,0 +1,60 @@ +import { + IconButton, + Menu, + MenuButton, + MenuItem, + MenuList, + Spinner, +} from "@chakra-ui/react"; +import { useState } from "react"; + +export interface IconButtonMenuItems { + name: string; + value: string; +} +type IconButtonMenuProbs = { + icon: React.ReactElement; + onOpen: () => Promise; + onClick: (value: string) => void; +}; + +export const IconButtonMenu = ({ + icon, + onOpen, + onClick, +}: IconButtonMenuProbs) => { + const [items, setItems] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + const handleOnOpen = () => { + setIsLoading(true); + onOpen() + .then((values) => setItems(values)) + .finally(() => setIsLoading(false)); + }; + return ( + + + + {isLoading ? ( + + + + ) : ( + items.map((i, idx) => ( + onClick(i.value)}> + {i.name} + + )) + )} + + + ); +}; + +export default IconButtonMenu; diff --git a/src/components/password-input/index.tsx b/src/components/password-input/index.tsx index 4c700b8..dbc0c3d 100644 --- a/src/components/password-input/index.tsx +++ b/src/components/password-input/index.tsx @@ -11,11 +11,13 @@ import { Eye, EyeOff } from "react-feather"; type PasswordInputProbs = { password: [string, React.Dispatch>]; placeHolder?: string; + isRequired?: boolean; }; function PasswordInput({ password: [pass, setPass], placeHolder, + isRequired = false, }: PasswordInputProbs) { const [showPassword, setShowPassword] = useState(false); const handleClick = () => setShowPassword(!showPassword); @@ -27,6 +29,7 @@ function PasswordInput({ type={showPassword ? "text" : "password"} placeholder={placeHolder || ""} value={pass} + isRequired onChange={(e) => setPass(e.target.value)} /> diff --git a/src/storage/index.ts b/src/storage/index.ts index 86fc5e2..9cc3f00 100644 --- a/src/storage/index.ts +++ b/src/storage/index.ts @@ -1,4 +1,8 @@ -import { AppSettings, CallHistory } from "src/common/types"; +import { + AdvancedAppSettings, + AppSettings, + CallHistory, +} from "src/common/types"; import { Buffer } from "buffer"; // Settings @@ -21,6 +25,26 @@ export const getSettings = (): AppSettings => { return {} as AppSettings; }; +// Advanced settings +const ADVANCED_SETTINGS_KET = "AdvancedSettingsKey"; + +export const saveAddvancedSettings = (settings: AdvancedAppSettings) => { + const encoded = Buffer.from(JSON.stringify(settings), "utf-8").toString( + "base64" + ); + + localStorage.setItem(ADVANCED_SETTINGS_KET, encoded); +}; + +export const getAdvancedSettings = (): AdvancedAppSettings => { + const str = localStorage.getItem(ADVANCED_SETTINGS_KET); + if (str) { + const planText = Buffer.from(str, "base64").toString("utf-8"); + return JSON.parse(planText) as AdvancedAppSettings; + } + return {} as AdvancedAppSettings; +}; + // Call History const historyKey = "History"; const MAX_HISTORY_COUNT = 20; diff --git a/src/window/phone/index.tsx b/src/window/phone/index.tsx index c10538b..d664ad5 100644 --- a/src/window/phone/index.tsx +++ b/src/window/phone/index.tsx @@ -40,12 +40,16 @@ import GreenAvatar from "src/imgs/icons/Avatar-Green.svg"; import "./styles.scss"; import { deleteCurrentCall, + getAdvancedSettings, getCurrentCall, saveCallHistory, saveCurrentCall, } from "src/storage"; import { OutGoingCall } from "./outgoing-call"; import { v4 as uuidv4 } from "uuid"; +import IconButtonMenu, { IconButtonMenuItems } from "src/components/menu"; +import { Application } from "src/api/types"; +import { getApplications, getQueues, getRegisteredUser } from "src/api"; type PhoneProbs = { sipDomain: string; @@ -79,6 +83,7 @@ export const Phone = ({ const [isStatusDropdownDisabled, setIsStatusDropdownDisabled] = useState(false); const [isCallButtonLoading, setIsCallButtonLoading] = useState(false); + const [isAdvanceMode, setIsAdvancedMode] = useState(false); useEffect(() => { if (sipDomain && sipUsername && sipPassword && sipServerAddress) { @@ -88,6 +93,8 @@ export const Phone = ({ setIsConfigured(false); clientGoOffline(); } + const advancedSettings = getAdvancedSettings(); + setIsAdvancedMode(!!advancedSettings.accountSid); }, [sipDomain, sipUsername, sipPassword, sipServerAddress, sipDisplayName]); useEffect(() => { @@ -395,33 +402,86 @@ export const Phone = ({ mt={5} className={isOnline() ? "" : "blurred"} > - - - } - variant="unstyled" - onClick={handleCallOnHold} - /> - - - } - variant="unstyled" - onClick={handleCallOnHold} - /> - + {isAdvanceMode && ( + + + } + onClick={(value) => { + console.log(value); + }} + onOpen={() => { + return new Promise( + (resolve, reject) => { + getRegisteredUser() + .then(({ json }) => { + resolve( + json.map((u) => { + const uName = u.match(/(^.*)@.*/); + return { + name: uName ? uName[1] : u, + value: uName ? uName[1] : u, + }; + }) + ); + }) + .catch((err) => reject(err)); + } + ); + }} + /> + + + } + onClick={(value) => { + console.log(value); + }} + onOpen={() => { + return new Promise( + (resolve, reject) => { + getQueues() + .then(({ json }) => { + resolve( + json.map((q) => ({ + name: q, + value: q, + })) + ); + }) + .catch((err) => reject(err)); + } + ); + }} + /> + - - } - variant="unstyled" - onClick={handleCallOnHold} - /> - - + + } + onClick={(value) => { + console.log(value); + }} + onOpen={() => { + return new Promise( + (resolve, reject) => { + getApplications() + .then(({ json }) => { + resolve( + json.map((a) => ({ + name: a.name, + value: a.application_sid, + })) + ); + }) + .catch((err) => reject(err)); + } + ); + }} + /> + + + )} {isSipClientIdle(callStatus) ? ( { +export const AdvancedSettings = () => { const [apiKey, setApiKey] = useState(""); const [apiServer, setApiServer] = useState(""); - const handleSubmit = () => {}; - const resetSetting = () => {}; + const [accountSid, setAccountSid] = useState(""); + + useEffect(() => { + const settings = getAdvancedSettings(); + if (settings.apiServer) { + setApiServer(settings.apiServer); + } + if (settings.apiKey) { + setApiKey(settings.apiKey); + } + if (settings.accountSid) { + setAccountSid(settings.accountSid); + } + }, []); + + const handleSubmit = () => { + const settings: AdvancedAppSettings = { + accountSid, + apiKey, + apiServer, + }; + + saveAddvancedSettings(settings); + }; + const resetSetting = () => { + saveAddvancedSettings({} as AdvancedAppSettings); + setApiKey(""); + setApiServer(""); + setAccountSid(""); + }; + return (
@@ -29,7 +60,7 @@ export const AdvanceSettings = () => { w="full" p={0} > - + Jambonz API Server Base URL { onChange={(e) => setApiServer(e.target.value)} /> + + Jambonz Account Sid + setAccountSid(e.target.value)} + /> + API Key - +