Compare commits

...

5 Commits

Author SHA1 Message Date
Quan HL
9c5711dc68 filter by from and to 2023-06-09 07:20:48 +07:00
EgleH
724d86821d hint passwordSettings to user when creating password (#267)
Co-authored-by: eglehelms <e.helms@cognigy.com>
2023-06-08 07:24:29 -04:00
Hoan Luu Huu
f91bbe9245 clean tts cache for account (#264) 2023-06-02 07:39:48 -04:00
Hoan Luu Huu
91625612d5 feat: add seconds to recent call sumary (#261) 2023-06-01 08:23:00 -04:00
Hoan Luu Huu
fbe71925b4 feat: admin clear cache (#263)
* feat: admin clear cache

* feat: admin clear cache
2023-06-01 07:49:26 -04:00
11 changed files with 178 additions and 27 deletions

View File

@@ -248,3 +248,4 @@ export const API_SYSTEM_INFORMATION = `${API_BASE_URL}/SystemInformation`;
export const API_LCRS = `${API_BASE_URL}/Lcrs`;
export const API_LCR_ROUTES = `${API_BASE_URL}/LcrRoutes`;
export const API_LCR_CARRIER_SET_ENTRIES = `${API_BASE_URL}/LcrCarrierSetEntries`;
export const API_TTS_CACHE = `${API_BASE_URL}/TtsCache`;

View File

@@ -24,6 +24,7 @@ import {
API_LCR_ROUTES,
API_LCR_CARRIER_SET_ENTRIES,
API_LCRS,
API_TTS_CACHE,
} from "./constants";
import { ROUTE_LOGIN } from "src/router/routes";
import {
@@ -577,6 +578,14 @@ export const deleteLcrRoute = (sid: string) => {
return deleteFetch<EmptyResponse>(`${API_LCR_ROUTES}/${sid}`);
};
export const deleteTtsCache = () => {
return deleteFetch<EmptyResponse>(API_TTS_CACHE);
};
export const deleteAccountTtsCache = (sid: string) => {
return deleteFetch<EmptyResponse>(`${API_BASE_URL}/Accounts/${sid}/TtsCache`);
};
/** Named wrappers for `getFetch` */
export const getUser = (sid: string) => {

View File

@@ -123,6 +123,10 @@ export interface SystemInformation {
monitoring_domain_name: string;
}
export interface TtsCache {
size: number;
}
/** API responses/payloads */
export interface User {

View File

@@ -1,4 +1,4 @@
import React, { useState, useCallback } from "react";
import React, { useState, useCallback, useRef } from "react";
import { classNames } from "@jambonz/ui-kit";
import { Icons } from "src/components/icons";
@@ -7,14 +7,18 @@ import "./styles.scss";
type SearchFilterProps = JSX.IntrinsicElements["input"] & {
filter: [string, React.Dispatch<React.SetStateAction<string>>];
delay?: number | null;
};
export const SearchFilter = ({
placeholder,
filter: [filterValue, setFilterValue],
delay,
}: SearchFilterProps) => {
const [focus, setFocus] = useState(false);
const [tmpFilterValue, setTmpFilterValue] = useState(filterValue);
const [appearance, setAppearance] = useState(false);
const typingTimeoutRef = useRef<number | null>(null);
const classes = {
"search-filter": true,
focused: focus,
@@ -23,7 +27,18 @@ export const SearchFilter = ({
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setFilterValue(e.target.value.toLowerCase());
setTmpFilterValue(e.target.value.toLowerCase());
if (delay) {
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
typingTimeoutRef.current = setTimeout(() => {
setFilterValue(e.target.value.toLowerCase());
}, delay);
} else {
setFilterValue(e.target.value.toLowerCase());
}
if (e.target.value) {
setAppearance(true);
@@ -51,7 +66,7 @@ export const SearchFilter = ({
type="search"
name="search_filter"
placeholder={placeholder}
value={filterValue}
value={tmpFilterValue}
onChange={handleChange}
onFocus={() => {
setFocus(true);

View File

@@ -13,16 +13,6 @@ export const MSG_PASSWD_MATCH = "Passwords do not match";
export const MSG_SERVER_DOWN = "The server cannot be reached";
export const MSG_LOGGED_OUT = "You've successfully logged out.";
export const MSG_MUST_LOGIN = "You must log in to view that page";
export const MSG_PASSWD_CRITERIA = (
<>
Password must:
<ul>
<li>Be at least 6 characters</li>
<li>Contain at least one letter</li>
<li>Contain at least one number</li>
</ul>
</>
);
export const MSG_REQUIRED_FIELDS = (
<>
Fields marked with an asterisk<span>*</span> are required.

View File

@@ -7,7 +7,7 @@ import { useApiData } from "src/api";
import { toastError, useSelectState } from "src/store";
import { AccountForm } from "./form";
import type { Account, Application, Limit } from "src/api/types";
import type { Account, Application, Limit, TtsCache } from "src/api/types";
import {
ROUTE_INTERNAL_ACCOUNTS,
ROUTE_INTERNAL_APPLICATIONS,
@@ -25,6 +25,9 @@ export const EditAccount = () => {
`Accounts/${params.account_sid}/Limits`
);
const [apps] = useApiData<Application[]>("Applications");
const [ttsCache, ttsCacheFetcher] = useApiData<TtsCache>(
`Accounts/${params.account_sid}/TtsCache`
);
useScopedRedirect(
Scope.account,
@@ -50,6 +53,7 @@ export const EditAccount = () => {
apps={apps}
account={{ data, refetch, error }}
limits={{ data: limitsData, refetch: refetchLimits }}
ttsCache={{ data: ttsCache, refetch: ttsCacheFetcher }}
/>
<ApiKeys
path={`Accounts/${params.account_sid}/ApiKeys`}

View File

@@ -10,6 +10,7 @@ import {
useApiData,
postAccountLimit,
deleteAccountLimit,
deleteAccountTtsCache,
} from "src/api";
import { ClipBoard, Icons, Modal, Section, Tooltip } from "src/components";
import {
@@ -35,6 +36,7 @@ import type {
WebhookMethod,
UseApiDataMap,
Limit,
TtsCache,
} from "src/api/types";
import { hasLength } from "src/utils";
@@ -42,9 +44,15 @@ type AccountFormProps = {
apps?: Application[];
limits?: UseApiDataMap<Limit[]>;
account?: UseApiDataMap<Account>;
ttsCache?: UseApiDataMap<TtsCache>;
};
export const AccountForm = ({ apps, limits, account }: AccountFormProps) => {
export const AccountForm = ({
apps,
limits,
account,
ttsCache,
}: AccountFormProps) => {
const navigate = useNavigate();
const user = useSelectState("user");
const currentServiceProvider = useSelectState("currentServiceProvider");
@@ -60,6 +68,7 @@ export const AccountForm = ({ apps, limits, account }: AccountFormProps) => {
const [initialRegHook, setInitialRegHook] = useState(false);
const [initialQueueHook, setInitialQueueHook] = useState(false);
const [localLimits, setLocalLimits] = useState<Limit[]>([]);
const [clearTtsCacheFlag, setClearTtsCacheFlag] = useState(false);
/** This lets us map and render the same UI for each... */
const webhooks = [
@@ -139,6 +148,20 @@ export const AccountForm = ({ apps, limits, account }: AccountFormProps) => {
}
};
const handleClearCache = () => {
deleteAccountTtsCache(account?.data?.account_sid || "")
.then(() => {
if (ttsCache) {
ttsCache.refetch();
}
setClearTtsCacheFlag(false);
toastSuccess("Tts Cache successfully cleaned");
})
.catch((error) => {
toastError(error.msg);
});
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
@@ -446,6 +469,25 @@ export const AccountForm = ({ apps, limits, account }: AccountFormProps) => {
</fieldset>
);
})}
{ttsCache && (
<fieldset>
<ButtonGroup left>
<Button
onClick={(e: React.FormEvent) => {
e.preventDefault();
setClearTtsCacheFlag(true);
}}
small
disabled={ttsCache.data?.size === 0}
>
Clear TTS Cache
</Button>
</ButtonGroup>
<MS>{`There are ${
ttsCache.data ? ttsCache.data.size : 0
} cached TTS prompts`}</MS>
</fieldset>
)}
{message && (
<fieldset>
<Message message={message} />
@@ -476,6 +518,14 @@ export const AccountForm = ({ apps, limits, account }: AccountFormProps) => {
</P>
</Modal>
)}
{clearTtsCacheFlag && (
<Modal
handleSubmit={handleClearCache}
handleCancel={() => setClearTtsCacheFlag(false)}
>
<P>Are you sure you want to clean TTS cache for this account?</P>
</Modal>
)}
</>
);
};

View File

@@ -32,7 +32,7 @@ export const DetailsItem = ({ call }: DetailsItemProps) => {
<div className="item__info">
<div className="item__title">
<strong>
{dayjs(call.attempted_at).format("YYYY MM.DD hh:mm a")}
{dayjs(call.attempted_at).format("YYYY MM.DD hh:mm:ss a")}
</strong>
<span className="i txt--dark">
{call.direction === "inbound" ? (

View File

@@ -15,6 +15,7 @@ import {
Spinner,
Pagination,
SelectFilter,
SearchFilter,
} from "src/components";
import { hasLength, hasValue } from "src/utils";
import { DetailsItem } from "./details";
@@ -47,6 +48,8 @@ export const RecentCalls = () => {
const [dateFilter, setDateFilter] = useState("today");
const [directionFilter, setDirectionFilter] = useState("io");
const [statusFilter, setStatusFilter] = useState("all");
const [fromFilter, setFromFilter] = useState("");
const [toFilter, setToFilter] = useState("");
const [pageNumber, setPageNumber] = useState(1);
const [perPageFilter, setPerPageFilter] = useState("25");
@@ -64,6 +67,8 @@ export const RecentCalls = () => {
: { days: Number(dateFilter) }),
...(statusFilter !== "all" && { answered: statusFilter }),
...(directionFilter !== "io" && { direction: directionFilter }),
...(fromFilter && { from: fromFilter }),
...(toFilter && { to: toFilter }),
};
getRecentCalls(accountSid, payload)
@@ -94,7 +99,15 @@ export const RecentCalls = () => {
if (accountSid) {
handleFilterChange();
}
}, [accountSid, pageNumber, dateFilter, directionFilter, statusFilter]);
}, [
accountSid,
pageNumber,
dateFilter,
directionFilter,
statusFilter,
fromFilter,
toFilter,
]);
/** Reset page number when filters change */
useEffect(() => {
@@ -136,6 +149,16 @@ export const RecentCalls = () => {
filter={[statusFilter, setStatusFilter]}
options={statusSelection}
/>
<SearchFilter
placeholder="Filter From"
filter={[fromFilter, setFromFilter]}
delay={1000}
/>
<SearchFilter
placeholder="Filter To"
filter={[toFilter, setToFilter]}
delay={1000}
/>
</section>
<Section {...(hasLength(calls) && { slim: true })}>
<div className="list">

View File

@@ -1,22 +1,25 @@
import React, { useEffect, useState } from "react";
import { ButtonGroup, Button } from "@jambonz/ui-kit";
import { ButtonGroup, Button, MS, P } from "@jambonz/ui-kit";
import {
useApiData,
postPasswordSettings,
postSystemInformation,
deleteTtsCache,
} from "src/api";
import { PasswordSettings, SystemInformation } from "src/api/types";
import { PasswordSettings, SystemInformation, TtsCache } from "src/api/types";
import { toastError, toastSuccess } from "src/store";
import { Selector } from "src/components/forms";
import { hasValue } from "src/utils";
import { PASSWORD_LENGTHS_OPTIONS, PASSWORD_MIN } from "src/api/constants";
import { Modal } from "src/components";
export const AdminSettings = () => {
const [passwordSettings, passwordSettingsFetcher] =
useApiData<PasswordSettings>("PasswordSettings");
const [systemInformatin, systemInformationFetcher] =
const [systemInformation, systemInformationFetcher] =
useApiData<SystemInformation>("SystemInformation");
const [ttsCache, ttsCacheFetcher] = useApiData<TtsCache>("TtsCache");
// Min value is 8
const [minPasswordLength, setMinPasswordLength] = useState(PASSWORD_MIN);
const [requireDigit, setRequireDigit] = useState(false);
@@ -24,6 +27,19 @@ export const AdminSettings = () => {
const [domainName, setDomainName] = useState("");
const [sipDomainName, setSipDomainName] = useState("");
const [monitoringDomainName, setMonitoringDomainName] = useState("");
const [clearTtsCacheFlag, setClearTtsCacheFlag] = useState(false);
const handleClearCache = () => {
deleteTtsCache()
.then(() => {
ttsCacheFetcher();
setClearTtsCacheFlag(false);
toastSuccess("Tts Cache successfully cleaned");
})
.catch((error) => {
toastError(error.msg);
});
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
@@ -62,12 +78,12 @@ export const AdminSettings = () => {
setMinPasswordLength(passwordSettings.min_password_length);
}
}
if (hasValue(systemInformatin)) {
setDomainName(systemInformatin.domain_name);
setSipDomainName(systemInformatin.sip_domain_name);
setMonitoringDomainName(systemInformatin.monitoring_domain_name);
if (hasValue(systemInformation)) {
setDomainName(systemInformation.domain_name);
setSipDomainName(systemInformation.sip_domain_name);
setMonitoringDomainName(systemInformation.monitoring_domain_name);
}
}, [passwordSettings, systemInformatin]);
}, [passwordSettings, systemInformation]);
return (
<>
@@ -132,6 +148,23 @@ export const AdminSettings = () => {
<div>Password require special character</div>
</label>
</fieldset>
<fieldset>
<ButtonGroup left>
<Button
onClick={(e: React.FormEvent) => {
e.preventDefault();
setClearTtsCacheFlag(true);
}}
small
disabled={!ttsCache || ttsCache.size === 0}
>
Clear TTS Cache
</Button>
</ButtonGroup>
<MS>{`There are ${
ttsCache ? ttsCache.size : 0
} cached TTS prompts`}</MS>
</fieldset>
<fieldset>
<ButtonGroup left>
<Button onClick={handleSubmit} small>
@@ -139,6 +172,14 @@ export const AdminSettings = () => {
</Button>
</ButtonGroup>
</fieldset>
{clearTtsCacheFlag && (
<Modal
handleSubmit={handleClearCache}
handleCancel={() => setClearTtsCacheFlag(false)}
>
<P>Are you sure you want to clean TTS cache?</P>
</Modal>
)}
</>
);
};

View File

@@ -17,7 +17,6 @@ import {
MSG_SOMETHING_WRONG,
MSG_CAPSLOCK,
MSG_PASSWD_MATCH,
MSG_PASSWD_CRITERIA,
} from "src/constants";
import type { IMessage } from "src/store/types";
@@ -50,7 +49,22 @@ export const CreatePassword = () => {
}
if (passwdSettings && !isValidPasswd(password, passwdSettings)) {
setMessage(MSG_PASSWD_CRITERIA);
setMessage(
<>
Password must:
<ul>
<li>
Be at least {passwdSettings.min_password_length} characters long
</li>
{passwdSettings.require_digit && (
<li>Contain at least one number</li>
)}
{passwdSettings.require_special_character && (
<li>Contain at least one special character</li>
)}
</ul>
</>
);
return;
}