optimize ui loading and dashboard performance
This commit is contained in:
+85
-40
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Alert, Badge, Button, Field, Input } from './components/ui.jsx';
|
||||
import { Icon } from './components/layout.jsx';
|
||||
import { Icon, PageLoadingDialog } from './components/layout.jsx';
|
||||
import { api, explainApiError } from './api.js';
|
||||
import {
|
||||
formatCurrency,
|
||||
@@ -238,9 +238,13 @@ export default function App() {
|
||||
const [dashboardTrends, setDashboardTrends] = useState(null);
|
||||
const [activeCalls, setActiveCalls] = useState([]);
|
||||
const [activeCallsLoading, setActiveCallsLoading] = useState(false);
|
||||
const [activeCallsBlockingLoading, setActiveCallsBlockingLoading] = useState(false);
|
||||
const [activeCallsError, setActiveCallsError] = useState('');
|
||||
const [activeCallsUpdatedAt, setActiveCallsUpdatedAt] = useState(null);
|
||||
const [apiLoading, setApiLoading] = useState(true);
|
||||
const [apiError, setApiError] = useState('');
|
||||
const loadedPagesRef = useRef(new Set());
|
||||
const pageRequestRef = useRef({ id: 0, controller: null });
|
||||
const currentPermissions = useMemo(() => permissionSet(authUser), [authUser]);
|
||||
const canPermission = (permission) => can(currentPermissions, permission);
|
||||
const allowedNavGroups = useMemo(() => navGroups
|
||||
@@ -249,55 +253,86 @@ export default function App() {
|
||||
const ActivePage = pages[active];
|
||||
const activeLabel = useMemo(() => navGroups.flatMap((group) => group.items).find((item) => item.key === active)?.label, [active]);
|
||||
const canAccessPage = (key, permissions = currentPermissions) => canAll(permissions, PAGE_PERMISSIONS[key] || []);
|
||||
const refreshApi = async (user = authUser) => {
|
||||
const loadPageData = async (page = active, user = authUser, { force = false } = {}) => {
|
||||
const sourceUser = user && Array.isArray(user.permissions) ? user : authUser;
|
||||
if (!sourceUser) return;
|
||||
if (!force && loadedPagesRef.current.has(page)) {
|
||||
setApiLoading(false);
|
||||
setApiError('');
|
||||
return;
|
||||
}
|
||||
const granted = permissionSet(sourceUser);
|
||||
const skipped = Symbol('skipped');
|
||||
const loadIfAllowed = (permission, loader) => (can(granted, permission) ? loader() : Promise.resolve(skipped));
|
||||
pageRequestRef.current.controller?.abort('PAGE_CHANGED');
|
||||
const controller = new AbortController();
|
||||
const requestId = pageRequestRef.current.id + 1;
|
||||
pageRequestRef.current = { id: requestId, controller };
|
||||
setApiLoading(true);
|
||||
setApiError('');
|
||||
const [summary, trends, customerList, vendorList, customerGatewayList, vendorGatewayList, lineGroupList, rechargeList, userList, roleList, auditLogList] = await Promise.allSettled([
|
||||
loadIfAllowed('dashboard.view', api.dashboardSummary),
|
||||
loadIfAllowed('dashboard.view', api.dashboardTrends),
|
||||
loadIfAllowed('customers.view', api.customers),
|
||||
loadIfAllowed('vendors.view', api.vendors),
|
||||
loadIfAllowed('customer_gateways.view', api.customerGateways),
|
||||
loadIfAllowed('vendor_gateways.view', api.vendorGateways),
|
||||
loadIfAllowed('line_groups.view', api.landingLineGroups),
|
||||
loadIfAllowed('recharges.view', api.recharges),
|
||||
loadIfAllowed('users.view', api.users),
|
||||
loadIfAllowed('roles.view', api.roles),
|
||||
loadIfAllowed('audit.view', api.auditLogs),
|
||||
]);
|
||||
const failures = [summary, trends, customerList, vendorList, customerGatewayList, vendorGatewayList, lineGroupList, rechargeList, userList, roleList, auditLogList].filter((result) => result.status === 'rejected');
|
||||
if (summary.status === 'fulfilled' && summary.value !== skipped) setDashboardSummary(summary.value);
|
||||
if (trends.status === 'fulfilled' && trends.value !== skipped) setDashboardTrends(trends.value);
|
||||
if (customerList.status === 'fulfilled' && customerList.value !== skipped) setCustomerRows(customerList.value.map(normalizeCustomer));
|
||||
if (vendorList.status === 'fulfilled' && vendorList.value !== skipped) setVendorRows(vendorList.value.map(normalizeVendor));
|
||||
if (customerGatewayList.status === 'fulfilled' && customerGatewayList.value !== skipped) setCustomerGatewayRows(customerGatewayList.value.map(normalizeCustomerGateway));
|
||||
if (vendorGatewayList.status === 'fulfilled' && vendorGatewayList.value !== skipped) setVendorGatewayRows(vendorGatewayList.value.map(normalizeVendorGateway));
|
||||
if (lineGroupList.status === 'fulfilled' && lineGroupList.value !== skipped) setLandingLineGroupRows(lineGroupList.value.map(normalizeLandingLineGroup));
|
||||
if (rechargeList.status === 'fulfilled' && rechargeList.value !== skipped) setRechargeRows(rechargeList.value.items.map(normalizeRecharge));
|
||||
if (userList.status === 'fulfilled' && userList.value !== skipped) setUserRows(userList.value.map(normalizeUser));
|
||||
if (roleList.status === 'fulfilled' && roleList.value !== skipped) setRoleRows(roleList.value.map(normalizeRole));
|
||||
if (auditLogList.status === 'fulfilled' && auditLogList.value !== skipped) setLogRows(auditLogList.value.items.map(normalizeAuditLog));
|
||||
if (failures.length) {
|
||||
setApiError(explainApiError(failures[0].reason));
|
||||
const signalOptions = { signal: controller.signal };
|
||||
const definitions = [];
|
||||
const add = (permission, load, apply) => {
|
||||
if (can(granted, permission)) definitions.push({ load, apply });
|
||||
};
|
||||
if (page === 'dashboard') {
|
||||
add('dashboard.view', () => api.dashboardOverview(undefined, signalOptions), (value) => {
|
||||
setDashboardSummary(value.summary);
|
||||
setDashboardTrends(value.trends);
|
||||
});
|
||||
} else if (page === 'customers') {
|
||||
add('customers.view', () => api.customers(signalOptions), (rows) => setCustomerRows(rows.map(normalizeCustomer)));
|
||||
} else if (page === 'vendors') {
|
||||
add('vendors.view', () => api.vendors(signalOptions), (rows) => setVendorRows(rows.map(normalizeVendor)));
|
||||
} else if (page === 'customerGateways') {
|
||||
add('customers.view', () => api.customers(signalOptions), (rows) => setCustomerRows(rows.map(normalizeCustomer)));
|
||||
add('customer_gateways.view', () => api.customerGateways(signalOptions), (rows) => setCustomerGatewayRows(rows.map(normalizeCustomerGateway)));
|
||||
add('line_groups.view', () => api.landingLineGroups(signalOptions), (rows) => setLandingLineGroupRows(rows.map(normalizeLandingLineGroup)));
|
||||
} else if (page === 'vendorGateways') {
|
||||
add('vendors.view', () => api.vendors(signalOptions), (rows) => setVendorRows(rows.map(normalizeVendor)));
|
||||
add('vendor_gateways.view', () => api.vendorGateways(signalOptions), (rows) => setVendorGatewayRows(rows.map(normalizeVendorGateway)));
|
||||
} else if (page === 'vendorLineGroups') {
|
||||
add('line_groups.view', () => api.landingLineGroups(signalOptions), (rows) => setLandingLineGroupRows(rows.map(normalizeLandingLineGroup)));
|
||||
add('vendor_gateways.view', () => api.vendorGateways(signalOptions), (rows) => setVendorGatewayRows(rows.map(normalizeVendorGateway)));
|
||||
} else if (page === 'rechargeRecords') {
|
||||
add('recharges.view', () => api.recharges(signalOptions), (value) => setRechargeRows(value.items.map(normalizeRecharge)));
|
||||
} else if (page === 'users' || page === 'roles') {
|
||||
add('users.view', () => api.users(signalOptions), (rows) => setUserRows(rows.map(normalizeUser)));
|
||||
add('roles.view', () => api.roles(signalOptions), (rows) => setRoleRows(rows.map(normalizeRole)));
|
||||
} else if (page === 'operationLogs') {
|
||||
add('audit.view', () => api.auditLogs(signalOptions), (value) => setLogRows(value.items.map(normalizeAuditLog)));
|
||||
} else if (page === 'cdr') {
|
||||
add('customer_gateways.view', () => api.customerGateways(signalOptions), (rows) => setCustomerGatewayRows(rows.map(normalizeCustomerGateway)));
|
||||
add('vendor_gateways.view', () => api.vendorGateways(signalOptions), (rows) => setVendorGatewayRows(rows.map(normalizeVendorGateway)));
|
||||
} else if (page === 'qualityRuleDependencies') {
|
||||
add('customers.view', () => api.customers(signalOptions), (rows) => setCustomerRows(rows.map(normalizeCustomer)));
|
||||
add('line_groups.view', () => api.landingLineGroups(signalOptions), (rows) => setLandingLineGroupRows(rows.map(normalizeLandingLineGroup)));
|
||||
}
|
||||
const results = await Promise.allSettled(definitions.map((definition) => definition.load()));
|
||||
if (pageRequestRef.current.id !== requestId) return;
|
||||
const failures = [];
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === 'fulfilled') definitions[index].apply(result.value);
|
||||
else if (!controller.signal.aborted) failures.push(result.reason);
|
||||
});
|
||||
if (failures.length) setApiError(explainApiError(failures[0]));
|
||||
else loadedPagesRef.current.add(page);
|
||||
setApiLoading(false);
|
||||
};
|
||||
const refreshActiveCalls = async () => {
|
||||
const refreshApi = () => loadPageData(active, authUser, { force: true });
|
||||
const refreshActiveCalls = useCallback(async ({ silent = false } = {}) => {
|
||||
setActiveCallsLoading(true);
|
||||
if (!silent) setActiveCallsBlockingLoading(true);
|
||||
setActiveCallsError('');
|
||||
try {
|
||||
const response = await api.activeCalls();
|
||||
setActiveCalls(response.items || []);
|
||||
setActiveCallsUpdatedAt(new Date());
|
||||
} catch (error) {
|
||||
setActiveCallsError(explainApiError(error));
|
||||
} finally {
|
||||
setActiveCallsLoading(false);
|
||||
if (!silent) setActiveCallsBlockingLoading(false);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
@@ -305,7 +340,7 @@ export default function App() {
|
||||
.then((response) => {
|
||||
if (!mounted) return;
|
||||
setAuthUser(response.user);
|
||||
void refreshApi(response.user);
|
||||
void loadPageData('dashboard', response.user, { force: true });
|
||||
})
|
||||
.catch(() => {
|
||||
if (!mounted) return;
|
||||
@@ -343,7 +378,7 @@ export default function App() {
|
||||
if (firstAllowed) {
|
||||
setActive(firstAllowed.key);
|
||||
}
|
||||
await refreshApi(response.user);
|
||||
await loadPageData(firstAllowed?.key || 'dashboard', response.user, { force: true });
|
||||
};
|
||||
const handleLogout = async () => {
|
||||
await api.logout();
|
||||
@@ -363,10 +398,12 @@ export default function App() {
|
||||
setDashboardTrends(null);
|
||||
setActiveCalls([]);
|
||||
setActiveCallsError('');
|
||||
loadedPagesRef.current.clear();
|
||||
pageRequestRef.current.controller?.abort('LOGOUT');
|
||||
};
|
||||
const reloadAfterMutation = async (operation) => {
|
||||
await operation();
|
||||
await refreshApi();
|
||||
await loadPageData(active, authUser, { force: true });
|
||||
};
|
||||
const applyRechargeResult = (response) => {
|
||||
const normalized = normalizeRecharge(response);
|
||||
@@ -458,7 +495,7 @@ export default function App() {
|
||||
: active === 'rechargeRecords'
|
||||
? { rechargeRows, apiLoading, apiError, refreshApi }
|
||||
: active === 'dashboard'
|
||||
? { dashboardSummary, dashboardTrends, apiLoading, apiError, refreshApi, customerRows }
|
||||
? { dashboardSummary, dashboardTrends, apiLoading, apiError, refreshApi }
|
||||
: active === 'businessPrefixes'
|
||||
? { can: canPermission }
|
||||
: active === 'numberLibrary'
|
||||
@@ -467,7 +504,9 @@ export default function App() {
|
||||
? {
|
||||
activeCalls,
|
||||
activeCallsLoading,
|
||||
activeCallsBlockingLoading,
|
||||
activeCallsError,
|
||||
activeCallsUpdatedAt,
|
||||
refreshActiveCalls,
|
||||
can: canPermission,
|
||||
onHangupActiveCall: async (id) => {
|
||||
@@ -478,7 +517,12 @@ export default function App() {
|
||||
: active === 'cdr'
|
||||
? { customerGatewayRows, vendorGatewayRows, can: canPermission }
|
||||
: active === 'quality'
|
||||
? { customerRows, lineGroupRows: landingLineGroupRows, can: canPermission }
|
||||
? {
|
||||
customerRows,
|
||||
lineGroupRows: landingLineGroupRows,
|
||||
can: canPermission,
|
||||
loadRuleDependencies: () => loadPageData('qualityRuleDependencies', authUser),
|
||||
}
|
||||
: active === 'users'
|
||||
? { userRows, setUserRows, roleRows, apiLoading, apiError, refreshApi, can: canPermission, onDeleteUser: (id) => reloadAfterMutation(() => api.deleteUser(id)) }
|
||||
: active === 'roles'
|
||||
@@ -527,7 +571,7 @@ export default function App() {
|
||||
<div className="nav-group" key={group.title}>
|
||||
<p>{group.title}</p>
|
||||
{group.items.filter((item) => !item.pending).map((item) => (
|
||||
<button key={item.key} className={active === item.key ? 'active' : ''} onClick={() => setActive(item.key)} title={`${item.label}${item.pending ? '(待设计)' : ''}`}>
|
||||
<button key={item.key} className={active === item.key ? 'active' : ''} onClick={() => { setActive(item.key); void loadPageData(item.key); }} title={`${item.label}${item.pending ? '(待设计)' : ''}`}>
|
||||
<span className="nav-short" aria-hidden="true">{item.label.slice(0, 1)}</span>
|
||||
<span className="nav-label">{item.label}</span>
|
||||
{item.pending ? <span className="nav-status">待设计</span> : null}
|
||||
@@ -553,6 +597,7 @@ export default function App() {
|
||||
<main className="page-content">
|
||||
{canAccessPage(active) ? <ActivePage {...activePageProps} /> : <NoPermissionPage label={activeLabel} />}
|
||||
</main>
|
||||
<PageLoadingDialog loading={apiLoading && active !== 'activeCalls'} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user