Feature/persist state (#191)

* persist SP implementation

* add account filter check after refresh

* store recent calls and alerts filter

---------

Co-authored-by: EgleHelms <e.helms@cognigy.com>
This commit is contained in:
EgleH
2023-02-02 15:53:04 +01:00
committed by GitHub
parent 51c2285bfb
commit af3e724240
17 changed files with 184 additions and 6 deletions
+5 -1
View File
@@ -5,6 +5,7 @@ import { Icons } from "src/components/icons";
import type { Account } from "src/api/types";
import { hasLength, sortLocaleName } from "src/utils";
import { setAccountFilter } from "src/store/localStore";
export type AccountFilterProps = {
label?: string;
@@ -43,7 +44,10 @@ export const AccountFilter = ({
id="account_filter"
name="account_filter"
value={accountSid}
onChange={(e) => setAccountSid(e.target.value)}
onChange={(e) => {
setAccountSid(e.target.value);
setAccountFilter(e.target.value);
}}
onFocus={() => setFocus(true)}
onBlur={() => setFocus(false)}
>
+7
View File
@@ -4,6 +4,8 @@ import { classNames } from "jambonz-ui";
import { Icons } from "src/components/icons";
import type { SelectorOption } from "./forms/selector";
import { setQueryFilter } from "src/store/localStore";
import { createFilterString } from "src/utils";
type SelectFilterProps = {
id: string;
@@ -38,6 +40,11 @@ export const SelectFilter = ({
value={filterValue}
onChange={(e) => {
setFilterValue(e.target.value);
const advancedFilter = createFilterString(
e.target.value,
label as string
);
setQueryFilter(advancedFilter);
if (handleSelect) {
handleSelect(e);
+8 -2
View File
@@ -11,6 +11,7 @@ import {
toastSuccess,
toastError,
} from "src/store";
import { getActiveSP, setActiveSP } from "src/store/localStore";
import { postServiceProviders } from "src/api";
import type { NaviItem } from "./items";
@@ -98,6 +99,7 @@ export const Navi = ({
);
dispatch({ type: "serviceProviders" });
setSid(json.sid);
setActiveSP(json.sid);
setName("");
setModal(false);
})
@@ -113,6 +115,7 @@ export const Navi = ({
/** Subscribe to change events on the service provider <select> */
useEffect(() => {
setSid(getActiveSP());
if (sid) {
const serviceProvider = serviceProviders.find(
(sp) => sp.service_provider_sid === sid
@@ -153,8 +156,11 @@ export const Navi = ({
<div className="smsel smsel--navi">
<div>
<select
value={currentServiceProvider?.service_provider_sid}
onChange={(e) => setSid(e.target.value)}
value={sid || currentServiceProvider?.service_provider_sid}
onChange={(e) => {
setSid(e.target.value);
setActiveSP(e.target.value);
}}
disabled={user?.scope !== USER_ADMIN}
>
{currentServiceProvider ? (
+15 -1
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useMemo, useState } from "react";
import { ButtonGroup, H1, M, MS } from "jambonz-ui";
import dayjs from "dayjs";
@@ -22,6 +22,11 @@ import {
import type { Account, Alert, PageQuery } from "src/api/types";
import { ScopedAccess } from "src/components/scoped-access";
import { Scope } from "src/store/types";
import {
getAccountFilter,
getQueryFilter,
setLocation,
} from "src/store/localStore";
export const Alerts = () => {
const user = useSelectState("user");
@@ -57,7 +62,16 @@ export const Alerts = () => {
});
};
useMemo(() => {
if (getQueryFilter()) {
const [date] = getQueryFilter().split("/");
setAccountSid(getAccountFilter());
setDateFilter(date);
}
}, [accountSid]);
useEffect(() => {
setLocation();
if (user?.account_sid && user.scope === USER_ACCOUNT) {
setAccountSid(user?.account_sid);
}
@@ -50,6 +50,7 @@ import type {
} from "src/api/types";
import { MSG_REQUIRED_FIELDS, MSG_WEBHOOK_FIELDS } from "src/constants";
import { isUserAccountScope, useRedirect } from "src/utils";
import { setAccountFilter, setLocation } from "src/store/localStore";
type ApplicationFormProps = {
application?: UseApiDataMap<Application>;
@@ -172,6 +173,7 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
.then(() => {
toastSuccess("Application created successfully");
navigate(ROUTE_INTERNAL_APPLICATIONS);
setAccountFilter(accountSid);
})
.catch((error) => {
toastError(error.msg);
@@ -180,6 +182,7 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
};
useEffect(() => {
setLocation();
if (application && application.data) {
setApplicationName(application.data.name);
@@ -27,6 +27,7 @@ import type { Application, Account } from "src/api/types";
import { ScopedAccess } from "src/components/scoped-access";
import { Scope } from "src/store/types";
import { USER_ACCOUNT } from "src/api/constants";
import { getAccountFilter, setLocation } from "src/store/localStore";
export const Applications = () => {
const user = useSelectState("user");
@@ -68,10 +69,13 @@ export const Applications = () => {
};
useEffect(() => {
setLocation();
if (user?.account_sid && user.scope === USER_ACCOUNT) {
setAccountSid(user?.account_sid);
}
setAccountSid(getAccountFilter());
if (accountSid) {
setApiUrl(`Accounts/${accountSid}/Applications`);
}
@@ -58,6 +58,7 @@ import type {
Smpp,
Application,
} from "src/api/types";
import { setAccountFilter, setLocation } from "src/store/localStore";
type CarrierFormProps = {
carrier?: UseApiDataMap<Carrier>;
@@ -532,6 +533,7 @@ export const CarrierForm = ({
toastSuccess("Carrier created successfully");
navigate(ROUTE_INTERNAL_CARRIERS);
setAccountFilter(accountSid);
})
.catch((error) => {
toastError(error.msg);
@@ -541,6 +543,7 @@ export const CarrierForm = ({
};
useEffect(() => {
setLocation();
if (predefinedName && hasLength(predefinedCarriers)) {
const predefinedCarrierSid = predefinedCarriers.find(
(a) => a.name === predefinedName
@@ -35,6 +35,7 @@ import { DeleteCarrier } from "./delete";
import type { Account, Carrier, SipGateway, SmppGateway } from "src/api/types";
import { Scope } from "src/store/types";
import { getAccountFilter, setLocation } from "src/store/localStore";
export const Carriers = () => {
const user = useSelectState("user");
@@ -47,6 +48,7 @@ export const Carriers = () => {
const [filter, setFilter] = useState("");
const carriersFiltered = useMemo(() => {
setAccountSid(getAccountFilter());
if (user?.scope === USER_ACCOUNT) {
return carriers;
}
@@ -116,6 +118,7 @@ export const Carriers = () => {
};
useEffect(() => {
setLocation();
if (accountSid) {
setApiUrl(`Accounts/${accountSid}/VoipCarriers`);
} else if (currentServiceProvider) {
@@ -30,6 +30,7 @@ import type {
Carrier,
UseApiDataMap,
} from "src/api/types";
import { setAccountFilter, setLocation } from "src/store/localStore";
type PhoneNumberFormProps = {
phoneNumber?: UseApiDataMap<PhoneNumber>;
@@ -104,6 +105,7 @@ export const PhoneNumberForm = ({ phoneNumber }: PhoneNumberFormProps) => {
.then(() => {
toastSuccess("Phone number created successfully");
navigate(ROUTE_INTERNAL_PHONE_NUMBERS);
setAccountFilter(accountSid);
})
.catch((error) => {
toastError(error.msg);
@@ -112,6 +114,7 @@ export const PhoneNumberForm = ({ phoneNumber }: PhoneNumberFormProps) => {
};
useEffect(() => {
setLocation();
if (phoneNumber && phoneNumber.data) {
setPhoneNumberNum(phoneNumber.data.number);
@@ -33,6 +33,7 @@ import type { Account, PhoneNumber, Carrier, Application } from "src/api/types";
import { USER_ACCOUNT } from "src/api/constants";
import { ScopedAccess } from "src/components/scoped-access";
import { Scope } from "src/store/types";
import { getAccountFilter, setLocation } from "src/store/localStore";
export const PhoneNumbers = () => {
const user = useSelectState("user");
@@ -52,6 +53,7 @@ export const PhoneNumbers = () => {
const [accountSid, setAccountSid] = useState("");
const phoneNumbersFiltered = useMemo(() => {
setAccountSid(getAccountFilter());
return phoneNumbers
? phoneNumbers.filter(
(phn) => !accountSid || phn.account_sid === accountSid
@@ -106,6 +108,7 @@ export const PhoneNumbers = () => {
};
useEffect(() => {
setLocation();
if (user?.account_sid && user.scope === USER_ACCOUNT) {
setAccountSid(user?.account_sid);
}
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useMemo, useState } from "react";
import { ButtonGroup, H1, M, MS } from "jambonz-ui";
import dayjs from "dayjs";
@@ -22,6 +22,11 @@ import { DetailsItem } from "./details";
import type { Account, CallQuery, RecentCall } from "src/api/types";
import { ScopedAccess } from "src/components/scoped-access";
import { Scope } from "src/store/types";
import {
getAccountFilter,
getQueryFilter,
setLocation,
} from "src/store/localStore";
const directionSelection = [
{ name: "either", value: "io" },
@@ -73,7 +78,18 @@ export const RecentCalls = () => {
});
};
useMemo(() => {
if (getQueryFilter()) {
const [date, direction, status] = getQueryFilter().split("/");
setAccountSid(getAccountFilter());
setDateFilter(date);
setDirectionFilter(direction);
setStatusFilter(status);
}
}, [accountSid]);
useEffect(() => {
setLocation();
if (accountSid) {
handleFilterChange();
}
@@ -38,6 +38,7 @@ import { CredentialStatus } from "./status";
import type { RegionVendors, GoogleServiceKey, Vendor } from "src/vendor/types";
import type { Account, SpeechCredential, UseApiDataMap } from "src/api/types";
import { setAccountFilter, setLocation } from "src/store/localStore";
type SpeechServiceFormProps = {
credential?: UseApiDataMap<SpeechCredential>;
@@ -176,6 +177,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
.then(() => {
toastSuccess("Speech credential created successfully");
navigate(ROUTE_INTERNAL_SPEECH);
setAccountFilter(accountSid);
})
.catch((error) => {
toastError(error.msg);
@@ -185,6 +187,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
};
useEffect(() => {
setLocation();
if (credential && credential.data) {
if (credential.data.vendor) {
setVendor(credential.data.vendor);
@@ -25,6 +25,7 @@ import { CredentialStatus } from "./status";
import type { SpeechCredential, Account } from "src/api/types";
import { ScopedAccess } from "src/components/scoped-access";
import { Scope } from "src/store/types";
import { getAccountFilter, setLocation } from "src/store/localStore";
export const SpeechServices = () => {
const user = useSelectState("user");
@@ -37,6 +38,7 @@ export const SpeechServices = () => {
const [filter] = useState("");
const credentialsFiltered = useMemo(() => {
setAccountSid(getAccountFilter());
if (user?.scope === USER_ACCOUNT) {
return credentials;
}
@@ -83,6 +85,7 @@ export const SpeechServices = () => {
};
useEffect(() => {
setLocation();
if (accountSid) {
setApiUrl(`Accounts/${accountSid}/SpeechCredentials`);
} else if (currentServiceProvider) {
@@ -37,6 +37,7 @@ import type {
Account,
} from "src/api/types";
import type { IMessage } from "src/store/types";
import { setAccountFilter, setLocation } from "src/store/localStore";
type UserFormProps = {
user?: UseApiDataMap<User>;
@@ -126,6 +127,7 @@ export const UserForm = ({ user }: UserFormProps) => {
.then(() => {
toastSuccess("User created successfully");
navigate(ROUTE_INTERNAL_USERS);
setAccountFilter(accountSid);
})
.catch((error: { msg: IMessage }) => {
toastError(error.msg);
@@ -165,6 +167,7 @@ export const UserForm = ({ user }: UserFormProps) => {
/** Set current user data values if applicable -- e.g. "edit mode" */
useEffect(() => {
setLocation();
if (user && user.data) {
setName(user.data.name);
setForceChange(!!user.data.force_change);
@@ -1,4 +1,4 @@
import React, { useMemo, useState } from "react";
import React, { useEffect, useMemo, useState } from "react";
import { H1, Button, Icon } from "jambonz-ui";
import { Link } from "react-router-dom";
@@ -30,6 +30,7 @@ import type { Account, User } from "src/api/types";
import { useSelectState } from "src/store";
import { ScopedAccess } from "src/components/scoped-access";
import { Scope } from "src/store/types";
import { getAccountFilter, setLocation } from "src/store/localStore";
export const Users = () => {
const user = useSelectState("user");
@@ -41,6 +42,7 @@ export const Users = () => {
const [accounts] = useServiceProviderData<Account[]>("Accounts");
const usersFiltered = useMemo(() => {
setAccountSid(getAccountFilter());
const serviceProviderUsers = users?.filter((e) => {
return (
e.scope === USER_ADMIN ||
@@ -72,6 +74,10 @@ export const Users = () => {
sortUsersAlpha
);
useEffect(() => {
setLocation();
}, []);
return (
<>
<section className="mast">
+83
View File
@@ -0,0 +1,83 @@
/**
* The key used to store active Service Provider in localStorage
*/
const storeActiveSP = "activeServiceProvider";
/**
* Methods to get/set the token from local storage
*/
export const getActiveSP = () => {
return localStorage.getItem(storeActiveSP) || "";
};
export const setActiveSP = (sid: string) => {
localStorage.setItem(storeActiveSP, sid);
};
/**
* The key used to store active Filter in localStorage
*/
const storeAccountFilter = "accountFilter";
/**
* Methods to get/set the account selected in the filter from local storage
*/
export const getAccountFilter = () => {
checkLocation();
return localStorage.getItem(storeAccountFilter) || "";
};
export const setAccountFilter = (accountSid: string) => {
localStorage.setItem(storeAccountFilter, accountSid);
};
export const removeAccountFilter = () => {
return localStorage.removeItem(storeAccountFilter);
};
/**
* Methods to get/set the RecentCalls and Alerts selected filters from local storage
*/
const storeQueryFilter = "advancedFilter";
export const setQueryFilter = (combinedFilterString: string) => {
return localStorage.setItem(storeQueryFilter, combinedFilterString);
};
export const getQueryFilter = () => {
checkLocation();
return localStorage.getItem(storeQueryFilter) || "";
};
export const removeAdvancedFilter = () => {
return localStorage.removeItem(storeQueryFilter);
};
/**
* Methods to get/set the location from local storage
*/
const storeLocation = "location";
export const setLocation = () => {
return localStorage.setItem(
storeLocation,
window.location.pathname.split("/")[2]
);
};
export const getLocation = () => {
return localStorage.getItem(storeLocation);
};
export const checkLocation = () => {
const currentLocation = window.location.pathname.split("/")[2];
const storedLocation = getLocation();
if (currentLocation !== storedLocation) {
localStorage.removeItem(storeQueryFilter);
localStorage.removeItem(storeAccountFilter);
return;
}
};
+14
View File
@@ -26,6 +26,7 @@ import type {
UserScopes,
} from "src/api/types";
import type { UserData } from "src/store/types";
import { getQueryFilter } from "src/store/localStore";
export const hasValue = <Type>(
variable: Type | null | undefined
@@ -211,6 +212,19 @@ export const filterScopeOptions = (
return optionArray;
};
export const createFilterString = (filterValue: string, label: string) => {
const filterString = getQueryFilter().split("/");
if (label === "Date") {
filterString.splice(0, 0, filterValue);
} else if (label === "Direction") {
filterString.splice(1, 1, filterValue);
} else if (label === "Status") {
filterString.splice(2, 2, filterValue);
}
return filterString.join("/");
};
export {
withSuspense,
useMobileMedia,