From 613550b1547262e73ac12e1b5fbd162fb4168b17 Mon Sep 17 00:00:00 2001 From: jsers Date: Wed, 12 Aug 2026 18:43:00 +0800 Subject: [PATCH] feat(list): persist page in URL and reset to 1 on save/group switch Move list pagination from sessionStorage to URL ?page=N across list pages. Reset to page 1 after create/edit/batch ops and on business-group switch (via a groupSwitchCount counter). Clear row selection on group switch to prevent stale cross-group IDs reaching batch operations. Add urlPage util (getPageFromSearch/setPageInSearch/removePageFromSearch) with unit tests. Note: task/taskTpl tables drop column sortable/filterMode alongside the custom onChange override for pagination sync. --- src/pages/alertRules/Add.tsx | 3 +- src/pages/alertRules/Edit.tsx | 3 +- src/pages/alertRules/FormNG/index.tsx | 7 ++- src/pages/alertRules/List/ListNG.tsx | 40 +++++++++++++---- src/pages/alertRules/List/MoreOperations.tsx | 17 +++++-- src/pages/alertRules/List/constants.ts | 1 + src/pages/alertRules/List/index.tsx | 9 +++- src/pages/alertRules/index.tsx | 10 ++++- src/pages/dashboard/Detail/Title.tsx | 7 ++- src/pages/dashboard/List/BatchClone.tsx | 4 +- src/pages/dashboard/List/Header.tsx | 19 +++++++- src/pages/dashboard/List/index.tsx | 31 ++++++++----- src/pages/notificationChannels/constants.ts | 1 + src/pages/notificationChannels/pages/Add.tsx | 1 - src/pages/notificationChannels/pages/Edit.tsx | 1 - .../notificationChannels/pages/Form/index.tsx | 6 ++- .../pages/ListNG/index.tsx | 21 ++++----- src/pages/notificationRules/constants.ts | 1 + src/pages/notificationRules/pages/Add.tsx | 2 +- .../notificationRules/pages/Detail/index.tsx | 7 ++- src/pages/notificationRules/pages/Edit.tsx | 2 +- .../notificationRules/pages/Form/index.tsx | 6 ++- src/pages/notificationRules/pages/List.tsx | 20 ++++----- src/pages/recordingRules/PageTable.tsx | 43 +++++++++++++----- .../recordingRules/components/operateForm.tsx | 16 +++++-- src/pages/recordingRules/index.tsx | 10 ++++- src/pages/task/add.tsx | 8 ++-- src/pages/task/detail.tsx | 6 ++- src/pages/task/index.tsx | 32 ++++++++----- src/pages/task/result.tsx | 6 ++- src/pages/taskTpl/add.tsx | 5 ++- src/pages/taskTpl/clone.tsx | 5 ++- src/pages/taskTpl/detail.tsx | 7 +-- src/pages/taskTpl/index.tsx | 41 ++++++++++++----- src/pages/taskTpl/modify.tsx | 5 ++- src/pages/warning/shield/index.tsx | 32 ++++++++----- src/pages/warning/subscribe/ListNG.tsx | 36 ++++++++++----- src/pages/warning/subscribe/index.tsx | 10 ++++- src/utils/urlPage.test.ts | 45 +++++++++++++++++++ src/utils/urlPage.ts | 22 +++++++++ 40 files changed, 404 insertions(+), 144 deletions(-) create mode 100644 src/utils/urlPage.test.ts create mode 100644 src/utils/urlPage.ts diff --git a/src/pages/alertRules/Add.tsx b/src/pages/alertRules/Add.tsx index 4303de6039..e3674e0325 100644 --- a/src/pages/alertRules/Add.tsx +++ b/src/pages/alertRules/Add.tsx @@ -23,6 +23,7 @@ import PageLayout from '@/components/pageLayout'; import { generateQueryNameByIndex } from '@/components/QueryName'; import Form from './FormNG'; import { defaultValues } from './Form/constants'; +import { getPageFromSearch } from '@/utils/urlPage'; export default function Add() { const { t } = useTranslation('alertRules'); @@ -64,7 +65,7 @@ export default function Add() { } return ( - +
diff --git a/src/pages/alertRules/Edit.tsx b/src/pages/alertRules/Edit.tsx index 0e1ef4e96f..50843f76f7 100644 --- a/src/pages/alertRules/Edit.tsx +++ b/src/pages/alertRules/Edit.tsx @@ -24,6 +24,7 @@ import PageLayout from '@/components/pageLayout'; import { getWarningStrategy } from '@/services/warning'; import Form from './FormNG'; import { getAlertRulePure } from './services'; +import { getPageFromSearch } from '@/utils/urlPage'; export default function Edit() { const { t } = useTranslation('alertRules'); @@ -62,7 +63,7 @@ export default function Edit() { }, 2000); return ( - +
{!_.isEmpty(values) && }
); diff --git a/src/pages/alertRules/FormNG/index.tsx b/src/pages/alertRules/FormNG/index.tsx index 15cb70b02a..245aecd182 100644 --- a/src/pages/alertRules/FormNG/index.tsx +++ b/src/pages/alertRules/FormNG/index.tsx @@ -2,7 +2,7 @@ import React, { createContext, useContext, useEffect, useMemo, useState, useRef, import { Alert, Button, Card, Col, Form, Input, message, Row, Select, Space } from 'antd'; import { Sparkles, ChevronsUpDown, ChevronsDownUp, PanelRight } from 'lucide-react'; import { useTranslation } from 'react-i18next'; -import { Link, useHistory, useParams } from 'react-router-dom'; +import { Link, useHistory, useParams, useLocation } from 'react-router-dom'; import _ from 'lodash'; import { CommonStateContext } from '@/App'; @@ -12,6 +12,7 @@ import { DatasourceCateSelectV2 } from '@/components/DatasourceSelect'; import { addStrategy, EditStrategy } from '@/services/warning'; import { scrollToFirstError } from '@/utils'; import { IS_PLUS } from '@/utils/constant'; +import { getPageFromSearch } from '@/utils/urlPage'; import RouterPrompt from '@/components/RouterPrompt'; import { defaultValues } from '../Form/constants'; @@ -72,6 +73,7 @@ function AdvancedSettingsSection(props: { export default function FormNG(props: IProps) { const { type, initialValues, editable = true } = props; const history = useHistory(); + const location = useLocation(); const { bgid } = useParams<{ bgid: string }>(); const { t, i18n } = useTranslation('alertRules'); @@ -194,6 +196,7 @@ export default function FormNG(props: IProps) { const leaveAfterSave = useCallback(() => { allowNextRouteRef.current = true; setAllowedLeave(true); + // 新增/编辑保存后回列表第一页(不带 page 参数,URL 默认第一页) history.push('/alert-rules'); }, [history]); @@ -592,7 +595,7 @@ export default function FormNG(props: IProps) { {t('common:btn.save')} - + diff --git a/src/pages/alertRules/List/ListNG.tsx b/src/pages/alertRules/List/ListNG.tsx index 68ba3a7afd..f219206f41 100644 --- a/src/pages/alertRules/List/ListNG.tsx +++ b/src/pages/alertRules/List/ListNG.tsx @@ -6,7 +6,7 @@ import { TriangleAlert, CircleCheckBig } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useDebounceFn } from 'ahooks'; import _ from 'lodash'; -import { Link } from 'react-router-dom'; +import { Link, useHistory, useLocation } from 'react-router-dom'; import { CommonStateContext } from '@/App'; import { updateAlertRules, deleteStrategy } from '@/services/warning'; @@ -22,10 +22,11 @@ import localeCompare from '@/pages/dashboard/Renderer/utils/localeCompare'; import { getItems as getNotificationRules, RuleItem as NotificationRuleItem } from '@/pages/notificationRules/services'; import { NS as notificationRulesNS } from '@/pages/notificationRules/constants'; import { AlertRuleType, AlertRuleStatus } from '@/pages/alertRules/types'; -import { defaultColumnsConfigs, LOCAL_STORAGE_KEY } from '@/pages/alertRules/List/constants'; +import { defaultColumnsConfigs, LOCAL_STORAGE_KEY, FILTER_SESSION_STORAGE_KEY } from '@/pages/alertRules/List/constants'; import EventsDrawer, { Props as EventsDrawerProps } from '@/pages/alertRules/List/EventsDrawer'; import EvalRecordsDrawer, { Props as EvalRecordsDrawerProps } from '@/pages/alertRules/List/EvalRecordsDrawer'; import { matchTriggerType, TRIGGER_TYPE_OPTIONS, TriggerType } from '@/pages/alertRules/List/utils'; +import { getPageFromSearch, setPageInSearch, removePageFromSearch } from '@/utils/urlPage'; interface Filter { cate?: string; @@ -37,7 +38,6 @@ interface Filter { triggerType?: TriggerType; } -const FILTER_SESSION_STORAGE_KEY = 'alert-rules-filter'; const includesProm = (datasourceList: any[], ids?: number[]) => { return _.some(ids, (id) => { return _.some(datasourceList, (item) => { @@ -56,31 +56,52 @@ interface Props { setRefreshFlag?: (flag: string) => void; linkTarget?: string; emptyGuide?: React.ReactNode; + gids?: string; + groupSwitchCount?: number; } export default function AlertRules(props: Props) { const { t, i18n } = useTranslation('alertRules'); const { busiGroups, datasourceList, feats } = useContext(CommonStateContext); - const { hideBusinessGroupColumn, showRowSelection, readonly, headerExtra, data, loading, setRefreshFlag, linkTarget, emptyGuide } = props; + const { hideBusinessGroupColumn, showRowSelection, readonly, headerExtra, data, loading, setRefreshFlag, linkTarget, emptyGuide, gids, groupSwitchCount } = props; + const history = useHistory(); + const location = useLocation(); let defaultFilter = {} as Filter; - let defaultPage = 1; try { const saved = JSON.parse(window.sessionStorage.getItem(FILTER_SESSION_STORAGE_KEY) || '{}'); defaultFilter = saved; - defaultPage = saved.current || 1; } catch (e) { console.error(e); } + const defaultPage = getPageFromSearch(location.search); const [filter, setFilter] = useState(defaultFilter); const [current, setCurrent] = useState(defaultPage); const handleFilterChange = (newFilter: Filter) => { setFilter(newFilter); setCurrent(1); - window.sessionStorage.setItem(FILTER_SESSION_STORAGE_KEY, JSON.stringify({ ...newFilter, current: 1 })); + window.sessionStorage.setItem(FILTER_SESSION_STORAGE_KEY, JSON.stringify(newFilter)); + history.replace({ pathname: location.pathname, search: setPageInSearch(location.search, 1) }); }; + // 新增/编辑/批量操作保存成功后,记录的更新时间会变化,按更新时间倒序的列表会把刚操作的记录排到第一页, + // 因此保存后需要把页码重置为 1,避免停留在旧页码看不到刚操作的记录 + const resetPaging = () => { + setCurrent(1); + clearSelection(); + history.replace({ pathname: location.pathname, search: removePageFromSearch(location.search) }); + }; + // 切换业务组时重置到第一页(父组件在业务组选择源头递增计数触发,不依赖 gids 变化时序) + useEffect(() => { + if (groupSwitchCount) { + resetPaging(); + } + }, [groupSwitchCount]); const [queryValue, setQueryValue] = useState(defaultFilter.search); const [selectRowKeys, setSelectRowKeys] = useState([]); const [selectedRows, setSelectedRows] = useState[]>([]); + const clearSelection = () => { + setSelectRowKeys([]); + setSelectedRows([]); + }; const [visibleColumns, setVisibleColumns] = useState(() => getDefaultColumnsConfigs(defaultColumnsConfigs, LOCAL_STORAGE_KEY)); const pagination = usePagination({ PAGESIZE_KEY: 'alert-rules-pagesize' }); const [eventsDrawerProps, setEventsDrawerProps] = useState({ @@ -156,6 +177,7 @@ export default function AlertRules(props: Props) { className='table-text' to={{ pathname: `/alert-rules/edit/${record.id}`, + search: `?page=${current}`, }} target={linkTarget} > @@ -472,6 +494,7 @@ export default function AlertRules(props: Props) { selectRowKeys, selectedRows, getList: fetchData, + clearSelection, })} { setCurrent(pag.current || 1); - const saved = JSON.parse(window.sessionStorage.getItem(FILTER_SESSION_STORAGE_KEY) || '{}'); - window.sessionStorage.setItem(FILTER_SESSION_STORAGE_KEY, JSON.stringify({ ...saved, current: pag.current || 1 })); + history.replace({ pathname: location.pathname, search: setPageInSearch(location.search, pag.current || 1) }); }} loading={loading} dataSource={filterData()} diff --git a/src/pages/alertRules/List/MoreOperations.tsx b/src/pages/alertRules/List/MoreOperations.tsx index 2fc790f0eb..6efaf4032b 100644 --- a/src/pages/alertRules/List/MoreOperations.tsx +++ b/src/pages/alertRules/List/MoreOperations.tsx @@ -35,6 +35,7 @@ interface MoreOperationsProps { selectRowKeys: React.Key[]; selectedRows: any[]; getAlertRules: () => void; + clearSelection?: () => void; } const ignoreFields = [ @@ -67,7 +68,7 @@ const ignoreFields = [ export default function MoreOperations(props: MoreOperationsProps) { const { t } = useTranslation('alertRules'); - const { bgid, isLeaf, selectRowKeys, selectedRows, getAlertRules } = props; + const { bgid, isLeaf, selectRowKeys, selectedRows, getAlertRules, clearSelection } = props; const [isModalVisible, setisModalVisible] = useState(false); const { isPlus, busiGroups } = useContext(CommonStateContext); @@ -103,6 +104,7 @@ export default function MoreOperations(props: MoreOperationsProps) { onOk: () => { deleteStrategy(selectRowKeys as number[], bgid!).then(() => { message.success(t('batch.delete_success')); + clearSelection?.(); getAlertRules(); }); }, @@ -140,7 +142,10 @@ export default function MoreOperations(props: MoreOperationsProps) { CloneToBgids({ ids: selectRowKeys, busiGroups, - onOk: getAlertRules, + onOk: () => { + clearSelection?.(); + getAlertRules(); + }, }); }} > @@ -159,7 +164,10 @@ export default function MoreOperations(props: MoreOperationsProps) { gid: bgid!, ids: selectRowKeys, busiGroups, - onOk: getAlertRules, + onOk: () => { + clearSelection?.(); + getAlertRules(); + }, }); }} > @@ -211,6 +219,7 @@ export default function MoreOperations(props: MoreOperationsProps) { ); if (!res.err) { message.success(t('common:success.modify')); + clearSelection?.(); getAlertRules(); setisModalVisible(false); } else { @@ -227,6 +236,7 @@ export default function MoreOperations(props: MoreOperationsProps) { ); if (!res.err) { message.success(t('common:success.modify')); + clearSelection?.(); getAlertRules(); setisModalVisible(false); } else { @@ -245,6 +255,7 @@ export default function MoreOperations(props: MoreOperationsProps) { ); if (!res.err) { message.success(t('common:success.modify')); + clearSelection?.(); getAlertRules(); setisModalVisible(false); } else { diff --git a/src/pages/alertRules/List/constants.ts b/src/pages/alertRules/List/constants.ts index 953307e387..bfdd842173 100644 --- a/src/pages/alertRules/List/constants.ts +++ b/src/pages/alertRules/List/constants.ts @@ -18,6 +18,7 @@ import i18next from 'i18next'; * */ export const LOCAL_STORAGE_KEY = 'alertRules_columns_configs'; +export const FILTER_SESSION_STORAGE_KEY = 'alert-rules-filter'; export const defaultColumnsConfigs = [ { name: 'cur_event_count', diff --git a/src/pages/alertRules/List/index.tsx b/src/pages/alertRules/List/index.tsx index df5ab985d2..6e3d21bd3b 100644 --- a/src/pages/alertRules/List/index.tsx +++ b/src/pages/alertRules/List/index.tsx @@ -16,6 +16,7 @@ import ListNG from './ListNG'; interface ListProps { gids?: string; + groupSwitchCount?: number; } function HeaderExtra( @@ -23,12 +24,13 @@ function HeaderExtra( selectRowKeys?: React.Key[]; selectedRows?: AlertRuleType[]; getList?: () => void; + clearSelection?: () => void; }, ) { const { t } = useTranslation('alertRules'); const { businessGroup, groupedDatasourceList, reloadGroupedDatasourceList, datasourceCateOptions } = useContext(CommonStateContext); const history = useHistory(); - const { gids, selectRowKeys = [], selectedRows = [], getList } = props; + const { gids, selectRowKeys = [], selectedRows = [], getList, clearSelection } = props; return ( @@ -67,6 +69,7 @@ function HeaderExtra( selectRowKeys={selectRowKeys} selectedRows={selectedRows} getAlertRules={getList} + clearSelection={clearSelection} /> )} @@ -77,7 +80,7 @@ export default function List(props: ListProps) { const { t } = useTranslation('alertRules'); const { businessGroup, groupedDatasourceList, reloadGroupedDatasourceList, datasourceCateOptions } = useContext(CommonStateContext); const history = useHistory(); - const { gids } = props; + const { gids, groupSwitchCount } = props; const [refreshFlag, setRefreshFlag] = useState(_.uniqueId('refresh_')); const [data, setData] = useState[]>([]); const [loading, setLoading] = useState(false); @@ -107,6 +110,8 @@ export default function List(props: ListProps) { } data={data} loading={loading} diff --git a/src/pages/alertRules/index.tsx b/src/pages/alertRules/index.tsx index 839fe5f69e..e18b1728ac 100644 --- a/src/pages/alertRules/index.tsx +++ b/src/pages/alertRules/index.tsx @@ -38,6 +38,12 @@ export default function index() { const { businessGroup } = useContext(CommonStateContext); const { t } = useTranslation('alertRules'); const [gids, setGids] = useState(getDefaultGids(N9E_GIDS_LOCALKEY, businessGroup)); + const [groupSwitchCount, setGroupSwitchCount] = useState(0); + // 切换业务组时通知列表重置到第一页(在业务组选择源头触发,不依赖 gids 变化时序) + const handleSelectGids = (ids: string) => { + setGids(ids); + setGroupSwitchCount((count) => count + 1); + }; return ( : undefined} >
- - {businessGroup.ids ? : } + + {businessGroup.ids ? : }
); diff --git a/src/pages/dashboard/Detail/Title.tsx b/src/pages/dashboard/Detail/Title.tsx index 19643a4333..3631f0ea85 100644 --- a/src/pages/dashboard/Detail/Title.tsx +++ b/src/pages/dashboard/Detail/Title.tsx @@ -16,6 +16,7 @@ */ import React, { useContext, useEffect, useRef, useState } from 'react'; import { useHistory, useLocation, Link } from 'react-router-dom'; +import { getPageFromSearch } from '@/utils/urlPage'; import querystring from 'query-string'; import _ from 'lodash'; import moment from 'moment'; @@ -105,6 +106,8 @@ export default function Title(props: IProps) { const [variablesWithOptions] = useGlobalState('variablesWithOptions'); const query = querystring.parse(location.search); const { viewMode, __public__ } = query; + // 从列表进入详情时 URL 带 page 参数,返回列表时回到原页;其他入口回列表第一页 + const goListPath = props.gobackPath || (getPageFromSearch(location.search) > 1 ? `/dashboards?page=${getPageFromSearch(location.search)}` : '/dashboards'); // AI 分析仅在正常已保存的仪表盘下展示:匿名公开、模板预览、内置组件场景要么调不通 assistant 接口,要么没有真实 dashboard_id const showAiAnalysis = !isPreview && !isBuiltin && __public__ !== 'true' && !!dashboard.id; const isClickTrigger = useRef(false); @@ -193,7 +196,7 @@ export default function Title(props: IProps) { onClick={() => { if (allowedLeave) { goBack(history).catch(() => { - history.push(props.gobackPath || '/dashboards'); + history.push(goListPath); }); } else { routerPromptRef.current.showPrompt(); @@ -204,7 +207,7 @@ export default function Title(props: IProps) { )} {!hideGoList && ( - + {isBuiltin ? t('builtInComponents:title') : t('list')} {'/'} diff --git a/src/pages/dashboard/List/BatchClone.tsx b/src/pages/dashboard/List/BatchClone.tsx index 56991d35f2..ac7a6fb85d 100644 --- a/src/pages/dashboard/List/BatchClone.tsx +++ b/src/pages/dashboard/List/BatchClone.tsx @@ -25,11 +25,12 @@ import { boardsClones } from '@/services/dashboardV2'; interface IProps { board_ids: number[]; busiGroups: any[]; + onOk?: () => void; } function BatchClone(props: IProps & ModalWrapProps) { const { t } = useTranslation('dashboard'); - const { visible, destroy, board_ids, busiGroups } = props; + const { visible, destroy, board_ids, busiGroups, onOk } = props; const [importResult, setImportResult] = useState<{ name: string; msg: string }[]>(); return ( @@ -60,6 +61,7 @@ function BatchClone(props: IProps & ModalWrapProps) { setImportResult(dataSource); if (_.every(dataSource, (item) => !item.msg)) { message.success(t('common:success.clone')); + onOk?.(); destroy(); } }) diff --git a/src/pages/dashboard/List/Header.tsx b/src/pages/dashboard/List/Header.tsx index bbe29331d4..355dd91233 100644 --- a/src/pages/dashboard/List/Header.tsx +++ b/src/pages/dashboard/List/Header.tsx @@ -31,6 +31,7 @@ import BatchClone from './BatchClone'; interface IProps { gids?: string; selectRowKeys: any[]; + setSelectRowKeys: (keys: number[]) => void; refreshList: () => void; searchVal: string; onSearchChange: (val) => void; @@ -44,7 +45,19 @@ interface IProps { export default function Header(props: IProps) { const { businessGroup, busiGroups } = useContext(CommonStateContext); const { t } = useTranslation('dashboard'); - const { gids, selectRowKeys, refreshList, searchVal, onSearchChange, visibleColumns, setVisibleColumns, columnOptions, selectedBusinessGroup, setSelectedBusinessGroup } = props; + const { + gids, + selectRowKeys, + setSelectRowKeys, + refreshList, + searchVal, + onSearchChange, + visibleColumns, + setVisibleColumns, + columnOptions, + selectedBusinessGroup, + setSelectedBusinessGroup, + } = props; const [importData, setImportData] = React.useState<{ visible: boolean; busiId?: number; @@ -120,6 +133,9 @@ export default function Header(props: IProps) { BatchClone({ board_ids: selectRowKeys, busiGroups, + onOk: () => { + setSelectRowKeys([]); + }, }); } else { message.warning(t('batch.noSelected')); @@ -137,6 +153,7 @@ export default function Header(props: IProps) { onOk: async () => { removeDashboards(selectRowKeys).then(() => { message.success(t('common:success.delete')); + setSelectRowKeys([]); refreshList(); }); }, diff --git a/src/pages/dashboard/List/index.tsx b/src/pages/dashboard/List/index.tsx index 8cf89cc314..48bd26285f 100644 --- a/src/pages/dashboard/List/index.tsx +++ b/src/pages/dashboard/List/index.tsx @@ -18,12 +18,13 @@ * 仪表盘列表页面 */ import React, { useState, useEffect, useContext } from 'react'; -import { Link } from 'react-router-dom'; +import { Link, useHistory } from 'react-router-dom'; import { Button, Modal, Space, message, Tooltip } from 'antd'; import { FundViewOutlined, EditOutlined, ShareAltOutlined } from '@ant-design/icons'; import _ from 'lodash'; import { useTranslation } from 'react-i18next'; import { useUpdateEffect } from 'ahooks'; +import { getPageFromSearch, setPageInSearch, removePageFromSearch } from '@/utils/urlPage'; import { useLocation } from 'react-router-dom'; import queryString from 'query-string'; @@ -56,7 +57,6 @@ import './style.less'; const N9E_GIDS_LOCALKEY = 'N9E_BOARD_NODE_ID'; const SEARCH_SESSION_STORAGE_KEY = 'n9e_dashboard_search'; -const DASHBOARD_PAGE_SESSION_KEY = 'n9e_dashboard_page'; const PUBLIC_SELECT_GIDS_LOCALKEY = 'N9E_PUBLIC_SELECT_GIDS'; const getDefaultPublicSelectGids = (localKey: string) => { const valueStr = localStorage.getItem(localKey); @@ -67,16 +67,15 @@ const getDefaultPublicSelectGids = (localKey: string) => { export default function index() { const { t } = useTranslation('dashboard'); const { businessGroup, perms } = useContext(CommonStateContext); - const queryParams = queryString.parse(useLocation().search); + const history = useHistory(); + const location = useLocation(); + const queryParams = queryString.parse(location.search); const [gids, setGids] = useState(() => getDashboardCompatibleGids(getDefaultGidsInDashboard(queryParams, N9E_GIDS_LOCALKEY, businessGroup))); const [list, setList] = useState([]); const [selectRowKeys, setSelectRowKeys] = useState([]); const [refreshKey, setRefreshKey] = useState(_.uniqueId('refreshKey_')); const [searchVal, setsearchVal] = useState(sessionStorage.getItem(SEARCH_SESSION_STORAGE_KEY) || ''); - const [current, setCurrent] = useState(() => { - const saved = sessionStorage.getItem(DASHBOARD_PAGE_SESSION_KEY); - return saved ? Number(saved) : 1; - }); + const [current, setCurrent] = useState(() => getPageFromSearch(location.search)); const [selectedBusinessGroup, setSelectedBusinessGroup] = useState(getDefaultPublicSelectGids(PUBLIC_SELECT_GIDS_LOCALKEY)); // 目前只有公开仪表盘会用到 const [busiGroups, setBusiGroups] = useState([]); const pagination = usePagination({ PAGESIZE_KEY: 'dashboard-pagesize' }); @@ -89,8 +88,15 @@ export default function index() { setsearchVal(''); setCurrent(1); sessionStorage.removeItem(SEARCH_SESSION_STORAGE_KEY); - sessionStorage.removeItem(DASHBOARD_PAGE_SESSION_KEY); + history.replace({ pathname: location.pathname, search: removePageFromSearch(location.search) }); }, [businessGroup.ids]); + // 侧边栏切换业务组只更新本地 gids(不更新全局 businessGroup.ids),这里在业务组选择源头重置页码到第一页 + const handleSelectGids = (ids: string) => { + setGids(ids); + setCurrent(1); + setSelectRowKeys([]); + history.replace({ pathname: location.pathname, search: removePageFromSearch(location.search) }); + }; useEffect(() => { if (gids === '-1') { @@ -132,7 +138,7 @@ export default function index() {
{ setRefreshKey(_.uniqueId('refreshKey_')); }} @@ -153,7 +160,7 @@ export default function index() { setsearchVal(val); setCurrent(1); sessionStorage.setItem(SEARCH_SESSION_STORAGE_KEY, val); - sessionStorage.setItem(DASHBOARD_PAGE_SESSION_KEY, '1'); + history.replace({ pathname: location.pathname, search: setPageInSearch(location.search, 1) }); }} visibleColumns={visibleColumns} setVisibleColumns={setVisibleColumns} @@ -182,7 +189,7 @@ export default function index() { className='table-active-text' to={{ pathname: `/dashboards/${record.ident || record.id}`, - search: gids === '-1' ? '__public__=true' : '', // 加上 __public__ 参数,用于在详情页判断是否为公开仪表盘 + search: gids === '-1' ? `__public__=true&page=${current}` : `?page=${current}`, // __public__ 用于详情页判断公开仪表盘 }} > {text} @@ -395,7 +402,7 @@ export default function index() { current, onChange: (page: number) => { setCurrent(page); - sessionStorage.setItem(DASHBOARD_PAGE_SESSION_KEY, String(page)); + history.replace({ pathname: location.pathname, search: setPageInSearch(location.search, page) }); }, }} locale={{ diff --git a/src/pages/notificationChannels/constants.ts b/src/pages/notificationChannels/constants.ts index 5206a182a5..64f6f8151d 100644 --- a/src/pages/notificationChannels/constants.ts +++ b/src/pages/notificationChannels/constants.ts @@ -2,6 +2,7 @@ import i18next from 'i18next'; export const NS = 'notification-channels'; export const PERM = `/${NS}`; +export const FILTER_SESSION_STORAGE_KEY = 'notification-channels-filter'; export const DEFAULT_VALUES = { enable: true, param_config: { diff --git a/src/pages/notificationChannels/pages/Add.tsx b/src/pages/notificationChannels/pages/Add.tsx index 6d0a4824a9..515b8483c0 100644 --- a/src/pages/notificationChannels/pages/Add.tsx +++ b/src/pages/notificationChannels/pages/Add.tsx @@ -33,7 +33,6 @@ export default function Add() { } showBack - backPath={`/${NS}`} >
} showBack - backPath={`/${NS}`} >
{data ? ( diff --git a/src/pages/notificationChannels/pages/Form/index.tsx b/src/pages/notificationChannels/pages/Form/index.tsx index 22051bd988..71b32bca68 100644 --- a/src/pages/notificationChannels/pages/Form/index.tsx +++ b/src/pages/notificationChannels/pages/Form/index.tsx @@ -3,10 +3,11 @@ import _ from 'lodash'; import { Form, Space, Input, Switch, Button, Row, Col, Tooltip } from 'antd'; import { MinusCircleOutlined, PlusCircleOutlined, QuestionCircleOutlined } from '@ant-design/icons'; import { useTranslation } from 'react-i18next'; -import { Link } from 'react-router-dom'; +import { Link, useLocation } from 'react-router-dom'; import { SIZE } from '@/utils/constant'; import { scrollToFirstError } from '@/utils'; +import { getPageFromSearch } from '@/utils/urlPage'; import { Document } from '@/components/DocumentDrawer'; import Splitter from '@/components/Splitter'; @@ -30,6 +31,7 @@ interface Props { export default function FormCpt(props: Props) { const { t } = useTranslation(NS); + const location = useLocation(); const [form] = Form.useForm(); const requestType = Form.useWatch('request_type', form); const ident = Form.useWatch('ident', form); @@ -194,7 +196,7 @@ export default function FormCpt(props: Props) { }) } /> - + diff --git a/src/pages/notificationChannels/pages/ListNG/index.tsx b/src/pages/notificationChannels/pages/ListNG/index.tsx index 359dd75053..8d17da6bc4 100644 --- a/src/pages/notificationChannels/pages/ListNG/index.tsx +++ b/src/pages/notificationChannels/pages/ListNG/index.tsx @@ -1,9 +1,9 @@ -import React, { useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { Input, Select, Space, Button, Modal, Switch, message } from 'antd'; import { NotificationOutlined, PlusOutlined } from '@ant-design/icons'; import { useTranslation } from 'react-i18next'; import { map, upperCase, includes, filter } from 'lodash'; -import { Link } from 'react-router-dom'; +import { Link, useHistory, useLocation } from 'react-router-dom'; import { useRequest } from 'ahooks'; import moment from 'moment'; @@ -13,9 +13,10 @@ import { Import, Export } from '@/components/ExportImport'; import EnhancedTable, { getEnabledStatusColumn } from '@/components/EnhancedTable'; import { dateColumn, updateByColumn } from '@/components/EnhancedTable/columns'; -import { NS, getNotificationChannelTypes } from '../../constants'; +import { NS, getNotificationChannelTypes, FILTER_SESSION_STORAGE_KEY } from '../../constants'; import { getItems, getItem, putItem, deleteItems, postItems } from '../../services'; import { ChannelItem } from '../../types'; +import { getPageFromSearch, setPageInSearch } from '@/utils/urlPage'; interface Filter { search?: string; @@ -23,10 +24,10 @@ interface Filter { idents?: string[]; } -const FILTER_SESSION_STORAGE_KEY = 'notification-channels-filter'; - export default function index() { const { t } = useTranslation(NS); + const history = useHistory(); + const location = useLocation(); const channelTypes = getNotificationChannelTypes(); const pagination = usePagination({ PAGESIZE_KEY: 'notification-channels-pagesize' }); @@ -43,20 +44,20 @@ export default function index() { const { data, loading, run, mutate } = useRequest(getItems); let defaultFilter = {} as Filter; - let defaultPage = 1; try { const saved = JSON.parse(window.sessionStorage.getItem(FILTER_SESSION_STORAGE_KEY) || '{}'); defaultFilter = saved; - defaultPage = saved.current || 1; } catch (e) { console.error(e); } + const defaultPage = getPageFromSearch(location.search); const [filters, setFilters] = useState(defaultFilter); const [current, setCurrent] = useState(defaultPage); const handleFilterChange = (newFilter: Filter) => { setFilters(newFilter); setCurrent(1); - window.sessionStorage.setItem(FILTER_SESSION_STORAGE_KEY, JSON.stringify({ ...newFilter, current: 1 })); + window.sessionStorage.setItem(FILTER_SESSION_STORAGE_KEY, JSON.stringify(newFilter)); + history.replace({ pathname: location.pathname, search: setPageInSearch(location.search, 1) }); }; const [selectedRows, setSelectedRows] = useState([]); const [togglingId, setTogglingId] = useState(); @@ -247,6 +248,7 @@ export default function index() { className='block truncate' to={{ pathname: `/${NS}/edit/${record.id}`, + search: `?page=${current}`, }} > {val} @@ -356,8 +358,7 @@ export default function index() { pagination={{ ...pagination, current }} onChange={(pag) => { setCurrent(pag.current || 1); - const saved = JSON.parse(window.sessionStorage.getItem(FILTER_SESSION_STORAGE_KEY) || '{}'); - window.sessionStorage.setItem(FILTER_SESSION_STORAGE_KEY, JSON.stringify({ ...saved, current: pag.current || 1 })); + history.replace({ pathname: location.pathname, search: setPageInSearch(location.search, pag.current || 1) }); }} scroll={{ y: 'calc(100% - 42px)' }} /> diff --git a/src/pages/notificationRules/constants.ts b/src/pages/notificationRules/constants.ts index 26c5b02ab9..9ca9325153 100644 --- a/src/pages/notificationRules/constants.ts +++ b/src/pages/notificationRules/constants.ts @@ -3,6 +3,7 @@ import moment from 'moment'; export const NS = 'notification-rules'; export const CN = 'n9e-notification-rules'; export const PERM = `/${NS}`; +export const FILTER_SESSION_STORAGE_KEY = 'notification-rules-filter'; export const DEFAULT_VALUES = { enable: true, notify_configs: [ diff --git a/src/pages/notificationRules/pages/Add.tsx b/src/pages/notificationRules/pages/Add.tsx index 2bc0a4ac4c..d75420d449 100644 --- a/src/pages/notificationRules/pages/Add.tsx +++ b/src/pages/notificationRules/pages/Add.tsx @@ -14,7 +14,7 @@ export default function Add() { const history = useHistory(); return ( - +
{ diff --git a/src/pages/notificationRules/pages/Detail/index.tsx b/src/pages/notificationRules/pages/Detail/index.tsx index 085df76f59..1832c41bef 100644 --- a/src/pages/notificationRules/pages/Detail/index.tsx +++ b/src/pages/notificationRules/pages/Detail/index.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import _ from 'lodash'; import { Trans, useTranslation } from 'react-i18next'; -import { useParams, Link } from 'react-router-dom'; +import { useParams, Link, useLocation } from 'react-router-dom'; import { Row, Col, Select, Card, Space, Tag, Tooltip, Button } from 'antd'; import { InfoCircleOutlined, TeamOutlined } from '@ant-design/icons'; @@ -13,6 +13,7 @@ import { getBrainLicense } from 'plus:/components/License/services'; import { NS, CN } from '../../constants'; import { getNotifyStatistics, NotifyStatistics, getItem, RuleItem } from '../../services'; +import { getPageFromSearch } from '@/utils/urlPage'; import { UpIcon, DownIcon } from '../../components/Icon'; import Events from './Events'; import AlertRules from './AlertRules'; @@ -21,6 +22,7 @@ import SubscribeRules from './SubscribeRules'; export default function Detail() { const { t } = useTranslation(NS); const { id } = useParams<{ id: string }>(); + const location = useLocation(); const [days, setDays] = useState(7); const [notifyStatistics, setNotifyStatistics] = useState(); const [activeTabKey, setActiveTabKey] = useState('events'); @@ -66,7 +68,7 @@ export default function Detail() { }; return ( - +
@@ -98,6 +100,7 @@ export default function Detail() { diff --git a/src/pages/notificationRules/pages/Edit.tsx b/src/pages/notificationRules/pages/Edit.tsx index a13b388717..95ecdfdb48 100644 --- a/src/pages/notificationRules/pages/Edit.tsx +++ b/src/pages/notificationRules/pages/Edit.tsx @@ -29,7 +29,7 @@ export default function Add() { }, []); return ( - +
{data ? ( ([]); const [activeIndex, setActiveIndex] = useState(); @@ -327,7 +329,7 @@ export default function FormCpt(props: Props) { {onCancel ? ( ) : ( - + )} diff --git a/src/pages/notificationRules/pages/List.tsx b/src/pages/notificationRules/pages/List.tsx index 8c7215b79c..47d168386b 100644 --- a/src/pages/notificationRules/pages/List.tsx +++ b/src/pages/notificationRules/pages/List.tsx @@ -3,7 +3,7 @@ import { Space, Button, Switch, Modal, Input } from 'antd'; import { NotificationOutlined, SearchOutlined } from '@ant-design/icons'; import { useTranslation } from 'react-i18next'; import _ from 'lodash'; -import { Link, useHistory } from 'react-router-dom'; +import { Link, useHistory, useLocation } from 'react-router-dom'; import { IS_PLUS } from '@/utils/constant'; import PageLayout from '@/components/pageLayout'; @@ -16,35 +16,35 @@ import { getTeamInfoList } from '@/services/manage'; import usePagination from '@/components/usePagination'; import { getItems, putItem, deleteItems } from '../services'; -import { NS, CN, TABLE_PAGINATION_CACHE_KEY } from '../constants'; +import { NS, CN, TABLE_PAGINATION_CACHE_KEY, FILTER_SESSION_STORAGE_KEY } from '../constants'; import { RuleItem } from '../types'; +import { getPageFromSearch, setPageInSearch } from '@/utils/urlPage'; interface Filter { search?: string; } -const FILTER_SESSION_STORAGE_KEY = 'notification-rules-filter'; - export default function List() { const { t } = useTranslation(NS); const history = useHistory(); + const location = useLocation(); const pagination = usePagination({ PAGESIZE_KEY: TABLE_PAGINATION_CACHE_KEY }); const [loading, setLoading] = useState(false); let defaultFilter = {} as Filter; - let defaultPage = 1; try { const saved = JSON.parse(window.sessionStorage.getItem(FILTER_SESSION_STORAGE_KEY) || '{}'); defaultFilter = saved; - defaultPage = saved.current || 1; } catch (e) { console.error(e); } + const defaultPage = getPageFromSearch(location.search); const [filter, setFilter] = useState(defaultFilter); const [current, setCurrent] = useState(defaultPage); const handleFilterChange = (newFilter: Filter) => { setFilter(newFilter); setCurrent(1); - window.sessionStorage.setItem(FILTER_SESSION_STORAGE_KEY, JSON.stringify({ ...newFilter, current: 1 })); + window.sessionStorage.setItem(FILTER_SESSION_STORAGE_KEY, JSON.stringify(newFilter)); + history.replace({ pathname: location.pathname, search: setPageInSearch(location.search, 1) }); }; const [data, setData] = useState([]); const [userGroups, setUserGroups] = useState<{ id: number; name: string }[]>([]); @@ -154,8 +154,7 @@ export default function List() { pagination={{ ...pagination, current }} onChange={(pag) => { setCurrent(pag.current || 1); - const saved = JSON.parse(window.sessionStorage.getItem(FILTER_SESSION_STORAGE_KEY) || '{}'); - window.sessionStorage.setItem(FILTER_SESSION_STORAGE_KEY, JSON.stringify({ ...saved, current: pag.current || 1 })); + history.replace({ pathname: location.pathname, search: setPageInSearch(location.search, pag.current || 1) }); }} columns={[ { @@ -166,6 +165,7 @@ export default function List() { {val} @@ -250,7 +250,7 @@ export default function List() { ]} rowActions={(record) => ({ inline: [ - { key: 'edit', icon: 'edit', text: t('common:btn.edit'), onClick: () => history.push({ pathname: `/${NS}/edit/${record.id}` }) }, + { key: 'edit', icon: 'edit', text: t('common:btn.edit'), onClick: () => history.push({ pathname: `/${NS}/edit/${record.id}`, search: `?page=${current}` }) }, { key: 'clone', icon: 'copy', text: t('common:btn.clone'), onClick: () => window.open(`/${NS}/edit/${record.id}?mode=clone`) }, { key: 'delete', diff --git a/src/pages/recordingRules/PageTable.tsx b/src/pages/recordingRules/PageTable.tsx index 4e46408ef8..ad07d1eeb7 100644 --- a/src/pages/recordingRules/PageTable.tsx +++ b/src/pages/recordingRules/PageTable.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState, useMemo, useContext } from 'react'; import { Button, Modal, message, Dropdown, Switch, Select, Space } from 'antd'; -import { useHistory } from 'react-router-dom'; +import { useHistory, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { ColumnType } from 'antd/lib/table'; import _ from 'lodash'; @@ -19,9 +19,11 @@ import usePagination from '@/components/usePagination'; import EditModal from './components/editModal'; import Import from './components/Import'; import Export from './components/Export'; +import { getPageFromSearch, setPageInSearch, removePageFromSearch } from '@/utils/urlPage'; interface Props { gids?: string; + groupSwitchCount?: number; } interface Filter { @@ -30,7 +32,7 @@ interface Filter { disabled?: 0 | 1; } -const FILTER_SESSION_STORAGE_KEY = 'recording-rules-filter'; +export const FILTER_SESSION_STORAGE_KEY = 'recording-rules-filter'; const { confirm } = Modal; const exportIgnoreAttrsObj = { @@ -43,22 +45,26 @@ const exportIgnoreAttrsObj = { update_by: undefined, }; -const PageTable: React.FC = ({ gids }) => { +const PageTable: React.FC = ({ gids, groupSwitchCount = 0 }) => { const [severity] = useState(); const { t } = useTranslation('recordingRules'); const history = useHistory(); + const location = useLocation(); const [selectRowKeys, setSelectRowKeys] = useState([]); const [selectedRows, setSelectedRows] = useState([]); + const clearSelection = () => { + setSelectRowKeys([]); + setSelectedRows([]); + }; const { groupedDatasourceList, businessGroup, busiGroups } = useContext(CommonStateContext); let defaultFilter = {} as Filter; - let defaultPage = 1; try { const saved = JSON.parse(window.sessionStorage.getItem(FILTER_SESSION_STORAGE_KEY) || '{}'); defaultFilter = saved; - defaultPage = saved.current || 1; } catch (e) { console.error(e); } + const defaultPage = getPageFromSearch(location.search); const [query, setQuery] = useState(defaultFilter.query ?? ''); const [isModalVisible, setisModalVisible] = useState(false); const [currentStrategyDataAll, setCurrentStrategyDataAll] = useState([]); @@ -116,12 +122,24 @@ const PageTable: React.FC = ({ gids }) => { }; const handleClickEdit = (id, isClone = false) => { - history.push(`/recording-rules/edit/${id}${isClone ? '?mode=clone' : ''}`); + history.push(`/recording-rules/edit/${id}${isClone ? '?mode=clone&' : '?'}fromGids=${gids ?? '-2'}&page=${current}`); }; const refreshList = () => { getRecordingRules(); }; + // 重置到第一页:清除 URL 里的 page 参数(切换业务组时调用,保留其余筛选条件) + const resetPaging = () => { + setCurrent(1); + clearSelection(); + history.replace({ pathname: location.pathname, search: removePageFromSearch(location.search) }); + }; + // 切换业务组时重置到第一页(父组件在业务组选择源头递增计数触发,不依赖 gids 变化时序) + useEffect(() => { + if (groupSwitchCount) { + resetPaging(); + } + }, [groupSwitchCount]); const columns: ColumnType[] = _.concat([ { @@ -259,6 +277,7 @@ const PageTable: React.FC = ({ gids }) => { if (businessGroup.id) { deleteRecordingRule(selectRowKeys as number[], businessGroup.id).then(() => { message.success(t('common:success.delete')); + clearSelection(); refreshList(); }); } @@ -300,6 +319,7 @@ const PageTable: React.FC = ({ gids }) => { ); if (!res.err) { message.success(t('common:success.edit')); + clearSelection(); refreshList(); setisModalVisible(false); } else { @@ -329,7 +349,8 @@ const PageTable: React.FC = ({ gids }) => { onChange={(val) => { setDatasourceIds(val); setCurrent(1); - saveState({ datasourceIds: val, current: 1 }); + saveState({ datasourceIds: val }); + history.replace({ pathname: location.pathname, search: setPageInSearch(location.search, 1) }); }} > {_.map(groupedDatasourceList?.prometheus, (item) => ( @@ -344,7 +365,8 @@ const PageTable: React.FC = ({ gids }) => { onSearch={(val) => { setQuery(val); setCurrent(1); - saveState({ query: val, current: 1 }); + saveState({ query: val }); + history.replace({ pathname: location.pathname, search: setPageInSearch(location.search, 1) }); }} allowClear /> @@ -359,7 +381,8 @@ const PageTable: React.FC = ({ gids }) => { onChange={(val) => { setFilterDisabled(val); setCurrent(1); - saveState({ disabled: val, current: 1 }); + saveState({ disabled: val }); + history.replace({ pathname: location.pathname, search: setPageInSearch(location.search, 1) }); }} /> @@ -397,7 +420,7 @@ const PageTable: React.FC = ({ gids }) => { current, onChange: (page) => { setCurrent(page); - saveState({ current: page }); + history.replace({ pathname: location.pathname, search: setPageInSearch(location.search, page) }); }, }} loading={loading} diff --git a/src/pages/recordingRules/components/operateForm.tsx b/src/pages/recordingRules/components/operateForm.tsx index d546476b25..dc2ceaf77c 100644 --- a/src/pages/recordingRules/components/operateForm.tsx +++ b/src/pages/recordingRules/components/operateForm.tsx @@ -1,6 +1,7 @@ import React, { useContext, useEffect } from 'react'; import _ from 'lodash'; -import { useHistory } from 'react-router-dom'; +import { useHistory, useLocation } from 'react-router-dom'; +import queryString from 'query-string'; import { Form, Input, Button, Modal, message, Space, Select, notification } from 'antd'; import { useTranslation } from 'react-i18next'; import { useRequest } from 'ahooks'; @@ -16,6 +17,7 @@ import { CommonStateContext } from '@/App'; import CronPattern from '@/components/CronPattern'; import KVTagSelect, { validatorOfKVTagSelect } from '@/components/KVTagSelect'; import { getBusiGroups } from '@/components/BusinessGroup/services'; +import { getPageFromSearch } from '@/utils/urlPage'; const DATASOURCE_ALL = 0; @@ -34,6 +36,9 @@ const OperateForm: React.FC = ({ type, initialValues = {} }) => { const { t } = useTranslation('recordingRules'); const { i18n } = useTranslation(); const history = useHistory(); // 创建的时候默认选中的值 + const location = useLocation(); + // 编辑/克隆入口会带上原列表的业务组上下文(fromGids),保存后跳回原上下文而不是记录的 group_id + const fromGids = queryString.parse(location.search)?.fromGids; const [form] = Form.useForm(); const { groupedDatasourceList } = useContext(CommonStateContext); const { openAiChat } = useAiChatContext(); @@ -65,7 +70,7 @@ const OperateForm: React.FC = ({ type, initialValues = {} }) => { message.success(t('common:success.edit')); history.push({ pathname: goListPath, - search: `ids=${values.group_id}&isLeaf=true`, + search: fromGids ? `ids=${fromGids}&isLeaf=true` : `ids=${values.group_id}&isLeaf=true`, }); } } else { @@ -80,7 +85,7 @@ const OperateForm: React.FC = ({ type, initialValues = {} }) => { message.success(`${type === 2 ? t('common:success.clone') : t('common:success.add')}`); history.push({ pathname: goListPath, - search: `ids=${values.group_id}&isLeaf=true`, + search: fromGids ? `ids=${fromGids}&isLeaf=true` : `ids=${values.group_id}&isLeaf=true`, }); } else { message.error(t(msg)); @@ -213,7 +218,10 @@ const OperateForm: React.FC = ({ type, initialValues = {} }) => {
- + - +
diff --git a/src/pages/taskTpl/index.tsx b/src/pages/taskTpl/index.tsx index 39fac1599d..8090fd0fa7 100644 --- a/src/pages/taskTpl/index.tsx +++ b/src/pages/taskTpl/index.tsx @@ -15,12 +15,13 @@ * */ import React, { useState, useContext, useEffect } from 'react'; -import { Link, useHistory } from 'react-router-dom'; +import { Link, useHistory, useLocation } from 'react-router-dom'; import { Modal, Row, Col, Button, Dropdown, Menu, message, Space } from 'antd'; import { DownOutlined, CodeOutlined } from '@ant-design/icons'; import { ColumnProps } from 'antd/lib/table'; import _ from 'lodash'; import { useAntdTable } from 'ahooks'; +import { getPageFromSearch, setPageInSearch, removePageFromSearch } from '@/utils/urlPage'; import { useTranslation } from 'react-i18next'; import EnhancedTable from '@/components/EnhancedTable'; @@ -41,12 +42,10 @@ import UnBindTags from './unBindTags'; const N9E_GIDS_LOCALKEY = 'N9E_TASK_TPL_NODE_ID'; const SEARCH_SESSION_KEY = 'taskTpl_query'; -const PAGE_SESSION_KEY = 'taskTpl_page'; function getTableData(options: any, gids: string | undefined, query: string) { if (gids) { const ids = gids === '-2' ? undefined : gids; - sessionStorage.setItem(PAGE_SESSION_KEY, String(options.current)); return request(`/api/n9e/busi-groups/task-tpls`, { method: RequestMethod.Get, params: { @@ -65,6 +64,7 @@ function getTableData(options: any, gids: string | undefined, query: string) { const index = (_props: any) => { const { t, i18n } = useTranslation('common'); const history = useHistory(); + const location = useLocation(); const [query, setQuery] = useState(() => sessionStorage.getItem(SEARCH_SESSION_KEY) || ''); const { busiGroups, businessGroup } = useContext(CommonStateContext); const [selectedIds, setSelectedIds] = useState([] as any[]); @@ -73,12 +73,23 @@ const index = (_props: any) => { sessionStorage.setItem(SEARCH_SESSION_KEY, query); }, [query]); - const defaultPage = Number(sessionStorage.getItem(PAGE_SESSION_KEY) || '1'); - const { tableProps, refresh } = useAntdTable((options) => getTableData(options, gids, query), { + const defaultPage = getPageFromSearch(location.search); + const { + tableProps, + refresh, + pagination: tablePagination, + } = useAntdTable((options) => getTableData(options, gids, query), { refreshDeps: [gids, query], debounceWait: 300, defaultParams: [{ current: defaultPage, pageSize: 10 }], }); + // 切换业务组时重置到第一页(在业务组选择源头触发,不依赖 gids 变化时序) + const handleSelectGids = (ids: string) => { + setGids(ids); + tablePagination.changeCurrent(1); + setSelectedIds([]); + history.replace({ pathname: location.pathname, search: removePageFromSearch(location.search) }); + }; const pagination = usePagination({ PAGESIZE_KEY: 'job-tpls' }); function handleTagClick(tag: string) { @@ -95,6 +106,7 @@ const index = (_props: any) => { selectedIds, busiId: businessGroup.id, onOk: () => { + setSelectedIds([]); refresh(); }, }); @@ -114,6 +126,7 @@ const index = (_props: any) => { uniqueTags, busiId: businessGroup.id, onOk: () => { + setSelectedIds([]); refresh(); }, }); @@ -131,7 +144,7 @@ const index = (_props: any) => { const groupName = _.find(busiGroups, { id: record.group_id })?.name; return (
- {text} + {text} ID: {record.id} {showBusinessGroup && groupName && {groupName}} @@ -143,8 +156,8 @@ const index = (_props: any) => { ] as any, [ tagsColumn({ title: t('tpl.tags'), dataIndex: 'tags', maxWidth: 180, onTagClick: handleTagClick }), - updateByColumn({ title: t('common:table.update_by'), dataIndex: 'update_by', nickname: 'update_by_nickname' }), - dateColumn({ title: t('common:table.update_at'), dataIndex: 'update_at', unix: true, sortable: true }), + updateByColumn({ title: t('common:table.update_by'), dataIndex: 'update_by', nickname: 'update_by_nickname', filterMode: 'none' }), + dateColumn({ title: t('common:table.update_at'), dataIndex: 'update_at', unix: true }), ] as any, ); @@ -155,7 +168,7 @@ const index = (_props: any) => { doc='https://flashcat.cloud/docs/content/flashcat-monitor/nightingale-v9/usage/alert-notify/self-healing/self-healing-script/' >
- + {gids ? (
@@ -219,19 +232,19 @@ const index = (_props: any) => { { key: 'create', text: t('task.create'), - onClick: () => history.push({ pathname: `/job-tpls/add/task`, search: `tpl=${record.id}&gid=${record.group_id}` }), + onClick: () => history.push({ pathname: `/job-tpls/add/task`, search: `tpl=${record.id}&gid=${record.group_id}&page=${tableProps.pagination?.current || 1}` }), }, { key: 'edit', icon: 'edit', text: t('common:btn.edit'), - onClick: () => history.push({ pathname: `/job-tpls/${record.id}/modify`, search: `gid=${record.group_id}` }), + onClick: () => history.push({ pathname: `/job-tpls/${record.id}/modify`, search: `gid=${record.group_id}&page=${tableProps.pagination?.current || 1}` }), }, { key: 'clone', icon: 'copy', text: t('common:btn.clone'), - onClick: () => history.push({ pathname: `/job-tpls/${record.id}/clone`, search: `gid=${record.group_id}` }), + onClick: () => history.push({ pathname: `/job-tpls/${record.id}/clone`, search: `gid=${record.group_id}&page=${tableProps.pagination?.current || 1}` }), }, { key: 'delete', @@ -256,6 +269,10 @@ const index = (_props: any) => { })} actionColumn={{ title: t('table.operations'), width: 130 }} {...(tableProps as any)} + onChange={(pag: any, filters?: any, sorter?: any) => { + tableProps.onChange?.(pag, filters, sorter); + history.replace({ pathname: location.pathname, search: setPageInSearch(location.search, pag.current || 1) }); + }} rowSelection={{ selectedRowKeys: selectedIds, onChange: (selectedRowKeys) => { diff --git a/src/pages/taskTpl/modify.tsx b/src/pages/taskTpl/modify.tsx index 6d2b070020..4adc25307c 100644 --- a/src/pages/taskTpl/modify.tsx +++ b/src/pages/taskTpl/modify.tsx @@ -16,6 +16,7 @@ */ import React, { useState, useEffect, useContext } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; +import { getPageFromSearch } from '@/utils/urlPage'; import { Button, Spin, Card, message, Space } from 'antd'; import { RollbackOutlined, CopyOutlined } from '@ant-design/icons'; import _ from 'lodash'; @@ -47,7 +48,7 @@ const Modify = (props: any) => { message.success(t('msg.modify.success')); props.history.push({ pathname: `/job-tpls`, - search: `ids=${curBusiId}&isLeaf=true`, + search: `ids=${curBusiId}&isLeaf=true&page=${getPageFromSearch(location.search)}`, }); }); }; @@ -74,7 +75,7 @@ const Modify = (props: any) => { - history.push('/job-tpls')} /> + history.push(`/job-tpls?page=${getPageFromSearch(location.search)}`)} /> {t('tpl')} } diff --git a/src/pages/warning/shield/index.tsx b/src/pages/warning/shield/index.tsx index 71fb6699a2..0599881125 100644 --- a/src/pages/warning/shield/index.tsx +++ b/src/pages/warning/shield/index.tsx @@ -21,7 +21,7 @@ import { CloseCircleOutlined, ExclamationCircleOutlined, SearchOutlined } from ' import moment from 'moment'; import _ from 'lodash'; import { useTranslation } from 'react-i18next'; -import { useHistory, Link } from 'react-router-dom'; +import { useHistory, Link, useLocation } from 'react-router-dom'; import Tags from '@/components/TableTags/Tags'; import EnhancedTable, { getEnabledStatusColumn } from '@/components/EnhancedTable'; @@ -34,6 +34,7 @@ import RefreshIcon from '@/components/RefreshIcon'; import { DatasourceSelect } from '@/components/DatasourceSelect'; import { CommonStateContext } from '@/App'; import usePagination from '@/components/usePagination'; +import { getPageFromSearch, setPageInSearch, removePageFromSearch } from '@/utils/urlPage'; import { allCates, getCateDisplayLabel } from '@/components/AdvancedWrap/utils'; import DocumentDrawer from '@/components/DocumentDrawer'; import EmptyGuide from '@/components/EmptyGuide'; @@ -54,23 +55,23 @@ interface Filter { disabled?: 0 | 1; } -const FILTER_SESSION_STORAGE_KEY = 'alert-mutes-filter'; +export const FILTER_SESSION_STORAGE_KEY = 'alert-mutes-filter'; const DOC_PATH = 'https://flashcat.cloud/docs/content/flashcat-monitor/nightingale-v9/usage/alert-notify/rules/alert-mute/'; const Shield: React.FC = () => { const { t, i18n } = useTranslation('alertMutes'); const history = useHistory(); + const location = useLocation(); const { datasourceList, groupedDatasourceList, businessGroup, busiGroups, darkMode } = useContext(CommonStateContext); const [gids, setGids] = useState(getDefaultGids(N9E_GIDS_LOCALKEY, businessGroup)); let defaultFilter = {} as Filter; - let defaultPage = 1; try { const saved = JSON.parse(window.sessionStorage.getItem(FILTER_SESSION_STORAGE_KEY) || '{}'); defaultFilter = saved; - defaultPage = saved.current || 1; } catch (e) { console.error(e); } + const defaultPage = getPageFromSearch(location.search); const [query, setQuery] = useState(defaultFilter.query ?? ''); const [currentShieldDataAll, setCurrentShieldDataAll] = useState>([]); const [currentShieldData, setCurrentShieldData] = useState>([]); @@ -84,6 +85,12 @@ const Shield: React.FC = () => { const prev = JSON.parse(window.sessionStorage.getItem(FILTER_SESSION_STORAGE_KEY) || '{}'); window.sessionStorage.setItem(FILTER_SESSION_STORAGE_KEY, JSON.stringify({ ...prev, ...patch })); }; + // 切换业务组时清除 URL 里的页码,回到第一页(在业务组选择源头触发,不依赖 gids 变化时序) + const handleSelectGids = (ids: string) => { + setGids(ids); + setCurrent(1); + history.replace({ pathname: location.pathname, search: removePageFromSearch(location.search) }); + }; const columns: ColumnsType = [ { title: t('note'), @@ -95,7 +102,7 @@ const Shield: React.FC = () => { {data} @@ -329,7 +336,8 @@ const Shield: React.FC = () => { let val = e.target.value; setQuery(val); setCurrent(1); - saveState({ query: val, current: 1 }); + saveState({ query: val }); + history.replace({ pathname: location.pathname, search: setPageInSearch(location.search, 1) }); }; // 只有选中了叶子业务组才能新建屏蔽规则 @@ -347,7 +355,7 @@ const Shield: React.FC = () => { return ( } doc={DOC_PATH}>
- +
@@ -363,7 +371,8 @@ const Shield: React.FC = () => { onChange={(val) => { setDatasourceIds(val); setCurrent(1); - saveState({ datasourceIds: val, current: 1 }); + saveState({ datasourceIds: val }); + history.replace({ pathname: location.pathname, search: setPageInSearch(location.search, 1) }); }} /> { onChange={(val) => { setFilterDisabled(val); setCurrent(1); - saveState({ disabled: val, current: 1 }); + saveState({ disabled: val }); + history.replace({ pathname: location.pathname, search: setPageInSearch(location.search, 1) }); }} /> @@ -420,7 +430,7 @@ const Shield: React.FC = () => { pagination={{ ...pagination, current }} onChange={(pag) => { setCurrent(pag.current || 1); - saveState({ current: pag.current || 1 }); + history.replace({ pathname: location.pathname, search: setPageInSearch(location.search, pag.current || 1) }); }} loading={loading} dataSource={currentShieldData} @@ -465,7 +475,7 @@ const Shield: React.FC = () => { onClick: () => { history.push({ pathname: `/alert-mutes/edit/${record.id}`, - search: `?mode=clone&bgid=${record.group_id}`, + search: `?mode=clone&bgid=${record.group_id}&page=${current}`, }); }, }, diff --git a/src/pages/warning/subscribe/ListNG.tsx b/src/pages/warning/subscribe/ListNG.tsx index 7fc0a5ab13..5a52eb094d 100644 --- a/src/pages/warning/subscribe/ListNG.tsx +++ b/src/pages/warning/subscribe/ListNG.tsx @@ -2,7 +2,7 @@ import React, { useState, useContext, useEffect } from 'react'; import { Button, Input, message, Modal, Space, Switch, Tag, Tooltip, Select } from 'antd'; import { ExclamationCircleOutlined, SearchOutlined } from '@ant-design/icons'; import { ColumnsType } from 'antd/lib/table'; -import { Link, useHistory } from 'react-router-dom'; +import { Link, useHistory, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import _ from 'lodash'; @@ -26,6 +26,7 @@ import { getItems as getNotificationRules, RuleItem as NotificationRuleItem } fr import { useIsAuthorized } from '@/components/AuthorizationWrapper'; import { defaultColumnsConfigs, LOCAL_STORAGE_KEY, DOC_URL } from './constants'; +import { getPageFromSearch, setPageInSearch, removePageFromSearch } from '@/utils/urlPage'; import ScenarioList from './components/ScenarioList'; import './locale'; import './index.less'; @@ -39,7 +40,7 @@ interface Filter { disabled?: 0 | 1; } -const FILTER_SESSION_STORAGE_KEY = 'alert-subscribes-filter'; +export const FILTER_SESSION_STORAGE_KEY = 'alert-subscribes-filter'; const { confirm } = Modal; @@ -53,24 +54,26 @@ interface Props { loading: boolean; setRefreshFlag: (flag: string) => void; linkTarget?: string; + gids?: string; + groupSwitchCount?: number; } const Subscribe = (props: Props) => { const { t, i18n } = useTranslation('alertSubscribes'); const history = useHistory(); + const location = useLocation(); const { datasourceList, busiGroups, darkMode } = useContext(CommonStateContext); - const { hideBusinessGroupColumn, readonly, canCreate, headerExtra, data, loading, setRefreshFlag, linkTarget } = props; + const { hideBusinessGroupColumn, readonly, canCreate, headerExtra, data, loading, setRefreshFlag, linkTarget, gids, groupSwitchCount } = props; const [visibleColumns, setVisibleColumns] = useState(() => getDefaultColumnsConfigs(defaultColumnsConfigs, LOCAL_STORAGE_KEY)); const columnOptions = buildColumnOptions(defaultColumnsConfigs, t); let defaultFilter = {} as Filter; - let defaultPage = 1; try { const saved = JSON.parse(window.sessionStorage.getItem(FILTER_SESSION_STORAGE_KEY) || '{}'); defaultFilter = saved; - defaultPage = saved.current || 1; } catch (e) { console.error(e); } + const defaultPage = getPageFromSearch(location.search); const [query, setQuery] = useState(defaultFilter.query ?? ''); const [datasourceIds, setDatasourceIds] = useState(defaultFilter.datasourceIds); const [filterDisabled, setFilterDisabled] = useState<0 | 1 | undefined>(defaultFilter.disabled); @@ -79,6 +82,13 @@ const Subscribe = (props: Props) => { const prev = JSON.parse(window.sessionStorage.getItem(FILTER_SESSION_STORAGE_KEY) || '{}'); window.sessionStorage.setItem(FILTER_SESSION_STORAGE_KEY, JSON.stringify({ ...prev, ...patch })); }; + // 切换业务组时清除 URL 里的页码,回到第一页(父组件在业务组选择源头递增计数触发,不依赖 gids 变化时序) + useEffect(() => { + if (groupSwitchCount) { + setCurrent(1); + history.replace({ pathname: location.pathname, search: removePageFromSearch(location.search) }); + } + }, [groupSwitchCount]); const [notificationRules, setNotificationRules] = useState(); const notificationRulesAuthorized = useIsAuthorized([notificationRulesPerm]); @@ -94,6 +104,7 @@ const Subscribe = (props: Props) => { { onChange={(val) => { setDatasourceIds(val); setCurrent(1); - saveState({ datasourceIds: val, current: 1 }); + saveState({ datasourceIds: val }); + history.replace({ pathname: location.pathname, search: setPageInSearch(location.search, 1) }); }} /> { onChange={(e) => { setQuery(e.target.value); setCurrent(1); - saveState({ query: e.target.value, current: 1 }); + saveState({ query: e.target.value }); + history.replace({ pathname: location.pathname, search: setPageInSearch(location.search, 1) }); }} prefix={} placeholder={t('search_placeholder')} @@ -382,7 +395,8 @@ const Subscribe = (props: Props) => { onChange={(val) => { setFilterDisabled(val); setCurrent(1); - saveState({ disabled: val, current: 1 }); + saveState({ disabled: val }); + history.replace({ pathname: location.pathname, search: setPageInSearch(location.search, 1) }); }} /> @@ -410,7 +424,7 @@ const Subscribe = (props: Props) => { pagination={{ ...pagination, current }} onChange={(pag) => { setCurrent(pag.current || 1); - saveState({ current: pag.current || 1 }); + history.replace({ pathname: location.pathname, search: setPageInSearch(location.search, pag.current || 1) }); }} loading={loading} // 仅在「确实一条订阅都没有」时展示引导;搜索命中为空时回退到默认空态,避免误导 @@ -469,13 +483,13 @@ const Subscribe = (props: Props) => { key: 'edit', icon: 'edit', text: t('common:btn.edit'), - onClick: () => history.push({ pathname: `/alert-subscribes/edit/${record.id}` }), + onClick: () => history.push({ pathname: `/alert-subscribes/edit/${record.id}`, search: `?page=${current}` }), }, { key: 'clone', icon: 'copy', text: t('common:btn.clone'), - onClick: () => history.push({ pathname: `/alert-subscribes/edit/${record.id}`, search: 'mode=clone' }), + onClick: () => history.push({ pathname: `/alert-subscribes/edit/${record.id}`, search: `mode=clone&page=${current}` }), }, { key: 'delete', diff --git a/src/pages/warning/subscribe/index.tsx b/src/pages/warning/subscribe/index.tsx index 7df270f3f8..8e9500c8ba 100644 --- a/src/pages/warning/subscribe/index.tsx +++ b/src/pages/warning/subscribe/index.tsx @@ -26,6 +26,12 @@ export default function List() { const history = useHistory(); const { businessGroup } = useContext(CommonStateContext); const [gids, setGids] = useState(getDefaultGids(N9E_GIDS_LOCALKEY, businessGroup)); // -2: 所有告警策略 + const [groupSwitchCount, setGroupSwitchCount] = useState(0); + // 切换业务组时通知列表重置到第一页(在业务组选择源头触发,不依赖 gids 变化时序) + const handleSelectGids = (ids: string) => { + setGids(ids); + setGroupSwitchCount((count) => count + 1); + }; const [refreshFlag, setRefreshFlag] = useState(_.uniqueId('refresh_')); const [data, setData] = useState>([]); const [loading, setLoading] = useState(false); @@ -48,7 +54,7 @@ export default function List() { return ( } doc={DOC_URL}>
- +
diff --git a/src/utils/urlPage.test.ts b/src/utils/urlPage.test.ts new file mode 100644 index 0000000000..5d52515b2a --- /dev/null +++ b/src/utils/urlPage.test.ts @@ -0,0 +1,45 @@ +import { getPageFromSearch, setPageInSearch, removePageFromSearch } from './urlPage'; + +describe('getPageFromSearch', () => { + it('缺省或空 search 返回 1', () => { + expect(getPageFromSearch('')).toBe(1); + expect(getPageFromSearch('?')).toBe(1); + expect(getPageFromSearch('?foo=1')).toBe(1); + }); + + it('读取有效页码', () => { + expect(getPageFromSearch('?page=3')).toBe(3); + expect(getPageFromSearch('?foo=1&page=7')).toBe(7); + expect(getPageFromSearch('?page=10&bar=x')).toBe(10); + }); + + it('非法页码回退到 1', () => { + expect(getPageFromSearch('?page=abc')).toBe(1); + expect(getPageFromSearch('?page=0')).toBe(1); + expect(getPageFromSearch('?page=-2')).toBe(1); + expect(getPageFromSearch('?page=1.5')).toBe(1); + }); +}); + +describe('setPageInSearch', () => { + it('空 search 时仅输出 page', () => { + expect(setPageInSearch('', 3)).toBe('page=3'); + }); + + it('保留既有参数并更新 page', () => { + expect(setPageInSearch('?foo=1&page=2', 5)).toBe('foo=1&page=5'); + expect(setPageInSearch('?foo=1', 2)).toBe('foo=1&page=2'); + }); +}); + +describe('removePageFromSearch', () => { + it('移除 page 参数,保留其余参数', () => { + expect(removePageFromSearch('?foo=1&page=3')).toBe('foo=1'); + expect(removePageFromSearch('?page=3')).toBe(''); + }); + + it('无 page 时原样返回', () => { + expect(removePageFromSearch('?foo=1')).toBe('foo=1'); + expect(removePageFromSearch('')).toBe(''); + }); +}); diff --git a/src/utils/urlPage.ts b/src/utils/urlPage.ts new file mode 100644 index 0000000000..bc0155c8b5 --- /dev/null +++ b/src/utils/urlPage.ts @@ -0,0 +1,22 @@ +import queryString from 'query-string'; + +// 从 URL search 中读取页码(?page=N),缺省/非法时回退到 1 +export function getPageFromSearch(search: string): number { + const parsed = queryString.parse(search); + const page = Array.isArray(parsed.page) ? parsed.page[0] : parsed.page; + const num = Number(page); + return Number.isInteger(num) && num > 0 ? num : 1; +} + +// 在现有 search 上设置/更新 page 参数,保留其余参数(幂等) +export function setPageInSearch(search: string, page: number): string { + const parsed = queryString.parse(search); + return queryString.stringify({ ...parsed, page }); +} + +// 从 search 中移除 page 参数,保留其余参数(幂等) +export function removePageFromSearch(search: string): string { + const parsed = queryString.parse(search); + delete parsed.page; + return queryString.stringify(parsed); +}