From 70343314d4bdfc952dfa9d01b56e35b1235490b7 Mon Sep 17 00:00:00 2001 From: jsers Date: Tue, 1 Sep 2026 15:34:40 +0800 Subject: [PATCH] feat(alert-cur-events): add fullscreen live view Add a fullscreen wall mode for the active alert events list, toggled by a fullscreen button. In this mode the table fetches page 1 on a refresh cadence, disables selection/pagination/row actions, and highlights rows whose hash did not appear in the previous fetch. --- .../TimeRangePicker/AutoRefresh/index.tsx | 4 +- .../alertCurEvent/pages/List/AlertTable.tsx | 179 +++--- .../pages/List/FullscreenList.tsx | 68 +++ src/pages/alertCurEvent/pages/List/index.tsx | 508 ++++++++++-------- 4 files changed, 458 insertions(+), 301 deletions(-) create mode 100644 src/pages/alertCurEvent/pages/List/FullscreenList.tsx diff --git a/src/components/TimeRangePicker/AutoRefresh/index.tsx b/src/components/TimeRangePicker/AutoRefresh/index.tsx index acfadf63fe..613749fbb9 100644 --- a/src/components/TimeRangePicker/AutoRefresh/index.tsx +++ b/src/components/TimeRangePicker/AutoRefresh/index.tsx @@ -35,7 +35,7 @@ interface IProps { function Refresh(props: IProps, ref) { const intervalSecondsCache = props.localKey ? _.toNumber(window.localStorage.getItem(props.localKey)) : 0; - const [intervalSeconds, setIntervalSeconds] = useState(props.intervalSeconds || intervalSecondsCache); + const [intervalSeconds, setIntervalSeconds] = useState(props.intervalSeconds ?? intervalSecondsCache); const [visible, setVisible] = useState(false); const intervalRef = useRef | undefined>(); const removeRef = useRef(false); @@ -86,7 +86,7 @@ function Refresh(props: IProps, ref) { }, []); useEffect(() => { - setIntervalSeconds(props.intervalSeconds || intervalSecondsCache); + setIntervalSeconds(props.intervalSeconds ?? intervalSecondsCache); }, [props.intervalSeconds]); useImperativeHandle(ref, () => ({ diff --git a/src/pages/alertCurEvent/pages/List/AlertTable.tsx b/src/pages/alertCurEvent/pages/List/AlertTable.tsx index bbb826df85..c213fb22e2 100644 --- a/src/pages/alertCurEvent/pages/List/AlertTable.tsx +++ b/src/pages/alertCurEvent/pages/List/AlertTable.tsx @@ -33,6 +33,7 @@ interface IProps { setRefreshFlag: (refreshFlag: string) => void; tagDisplayMode: AlertEventTagsDisplayMode; alertEscalationEnable: boolean; + isFullscreen: boolean; } function formatDuration(ms: number) { @@ -59,7 +60,7 @@ function formatDuration(ms: number) { } export default function AlertTable(props: IProps) { - const { filter, setFilter, selectedRowKeys, setSelectedRowKeys, params, setRefreshFlag, tagDisplayMode, alertEscalationEnable } = props; + const { filter, setFilter, selectedRowKeys, setSelectedRowKeys, params, setRefreshFlag, tagDisplayMode, alertEscalationEnable, isFullscreen } = props; const history = useHistory(); const { t } = useTranslation(NS); const { datasourceList } = useContext(CommonStateContext); @@ -73,6 +74,18 @@ export default function AlertTable(props: IProps) { const lastInitiatedViewIdRef = useRef(null); const tableContainerRef = useRef(null); const [tableHeaderHeight, setTableHeaderHeight] = useState(); + const [newFullscreenEventKeys, setNewFullscreenEventKeys] = useState>(new Set()); + const previousFullscreenPageRef = useRef<{ queryKey: string; eventKeys: Set }>(); + // TimeRangePicker 会在刷新时向 range 写入 refreshFlag;它不参与接口筛选,不能让它重置新增事件的对比基准。 + const fullscreenQueryKey = JSON.stringify({ + ...params, + range: params.range ? _.omit(params.range, ['refreshFlag']) : params.range, + }); + + useEffect(() => { + previousFullscreenPageRef.current = undefined; + setNewFullscreenEventKeys(new Set()); + }, [fullscreenQueryKey, isFullscreen]); useEffect(() => { const parsed = queryString.parse(location.search); @@ -236,9 +249,13 @@ export default function AlertTable(props: IProps) { } as any); } + const getEventIdentity = (event: { hash: string }) => event.hash; + const fetchData = ({ current, pageSize }) => { + const requestQueryKey = fullscreenQueryKey; + const isFullscreenRequest = isFullscreen; const requestParams: any = { - p: current, + p: isFullscreen ? 1 : current, limit: pageSize, my_groups: String(params.my_groups) === 'true', ..._.omit(params, ['range', 'my_groups']), @@ -250,16 +267,29 @@ export default function AlertTable(props: IProps) { requestParams.etime = moment(parsedRange.end).unix(); } return getEvents(requestParams).then((res) => { + const list = res.dat.list ?? []; return { total: res.dat.total, - list: res.dat.list, + list, + requestQueryKey, + isFullscreenRequest, }; }); }; const { tableProps } = useAntdTable(fetchData, { - refreshDeps: [JSON.stringify(params), props.refreshFlag], + refreshDeps: [fullscreenQueryKey, props.refreshFlag, isFullscreen], defaultPageSize: 30, debounceWait: 500, + onSuccess: (data: { list: Array<{ hash: string }>; requestQueryKey: string; isFullscreenRequest: boolean }) => { + if (!data.isFullscreenRequest) return; + + const eventKeys = new Set(data.list.map(getEventIdentity)); + const previousPage = previousFullscreenPageRef.current; + const nextNewEventKeys = previousPage?.queryKey === data.requestQueryKey ? new Set([...eventKeys].filter((key) => !previousPage.eventKeys.has(key))) : new Set(); + + previousFullscreenPageRef.current = { queryKey: data.requestQueryKey, eventKeys }; + setNewFullscreenEventKeys(nextNewEventKeys); + }, }); const pagination = usePagination({ PAGESIZE_KEY: EVENTS_TABLE_PAGESIZE_CACHE_KEY }); @@ -293,71 +323,86 @@ export default function AlertTable(props: IProps) { rowKey={(record) => record.id} columns={columns} {...tableProps} - rowClassName={(record: { severity: number; is_recovered: number }) => { - return SEVERITY_COLORS[record.is_recovered ? 3 : record.severity - 1] + '-left-border'; + rowClassName={(record: { hash: string; severity: number; is_recovered: number }) => { + const severityClassName = SEVERITY_COLORS[record.is_recovered ? 3 : record.severity - 1] + '-left-border'; + const shouldHighlight = isFullscreen && newFullscreenEventKeys.has(getEventIdentity(record)); + // 复用主题 violet-3(--fc-violet-3),明暗模式各自取值,避免硬编码色值。 + return shouldHighlight ? `${severityClassName} children:!bg-violet-300` : severityClassName; }} - rowSelection={{ - selectedRowKeys: selectedRowKeys, - onChange(selectedRowKeys: number[]) { - setSelectedRowKeys(selectedRowKeys); - }, - }} - pagination={{ - ...pagination, - ...tableProps.pagination, - pageSizeOptions: ['30', '100', '200', '500'], - }} - rowActions={(record) => ({ - inline: _.compact([ - IS_PLUS && alertEscalationEnable - ? { - key: 'ack', - icon: record.status === 0 ? 'claim' : 'unclaim', - text: record.status === 0 ? t('claim') : t('unclaim'), - onClick: () => { - ackEvents([record.id], record.status === 0 ? 'ack' : 'unack').then(() => { - setRefreshFlag(_.uniqueId('refresh_')); - }); - }, - } - : undefined, - !_.includes(['firemap', 'northstar'], record?.rule_prod) - ? { - key: 'shield', - icon: 'shield', - text: t('shield'), - onClick: () => { - history.push({ - pathname: '/alert-mutes/add', - search: queryString.stringify({ - busiGroup: record.group_id, - prod: record.rule_prod, - cate: record.cate, - datasource_ids: [record.datasource_id], - tags: record.tags, - }), - }); - }, - } - : undefined, - { - key: 'delete', - icon: 'delete', - text: t('common:btn.delete'), - danger: true, - onClick: () => - deleteAlertEventsModal( - [record.id], - () => { - setSelectedRowKeys(selectedRowKeys.filter((key) => key !== record.id)); - setRefreshFlag(_.uniqueId('refresh_')); + rowSelection={ + isFullscreen + ? undefined + : { + selectedRowKeys: selectedRowKeys, + onChange(selectedRowKeys: number[]) { + setSelectedRowKeys(selectedRowKeys); + }, + } + } + pagination={ + isFullscreen + ? false + : { + ...pagination, + ...tableProps.pagination, + pageSizeOptions: ['30', '100', '200', '500'], + } + } + rowActions={ + isFullscreen + ? undefined + : (record) => ({ + inline: _.compact([ + IS_PLUS && alertEscalationEnable + ? { + key: 'ack', + icon: record.status === 0 ? 'claim' : 'unclaim', + text: record.status === 0 ? t('claim') : t('unclaim'), + onClick: () => { + ackEvents([record.id], record.status === 0 ? 'ack' : 'unack').then(() => { + setRefreshFlag(_.uniqueId('refresh_')); + }); + }, + } + : undefined, + !_.includes(['firemap', 'northstar'], record?.rule_prod) + ? { + key: 'shield', + icon: 'shield', + text: t('shield'), + onClick: () => { + history.push({ + pathname: '/alert-mutes/add', + search: queryString.stringify({ + busiGroup: record.group_id, + prod: record.rule_prod, + cate: record.cate, + datasource_ids: [record.datasource_id], + tags: record.tags, + }), + }); + }, + } + : undefined, + { + key: 'delete', + icon: 'delete', + text: t('common:btn.delete'), + danger: true, + onClick: () => + deleteAlertEventsModal( + [record.id], + () => { + setSelectedRowKeys(selectedRowKeys.filter((key) => key !== record.id)); + setRefreshFlag(_.uniqueId('refresh_')); + }, + t, + ), }, - t, - ), - }, - ]) as any, - })} - actionColumn={{ title: t('common:table.operations'), width: 100 }} + ]) as any, + }) + } + actionColumn={isFullscreen ? undefined : { title: t('common:table.operations'), width: 100 }} /> void; + onRefresh: () => void; + onExit: () => void; +} + +export default function FullscreenList({ title, children, range, onRangeChange, onRefresh, onExit }: IProps) { + const { t } = useTranslation(NS); + const [refreshInterval, setRefreshInterval] = useState(30); + const containerRef = useRef(null); + + // 退出全屏时把自动刷新缓存置为 Off。该 key 与常规列表的 TimeRangePickerWithRefresh 共享, + // 因此离开全屏大屏后常规列表也会停止轮询;AutoRefresh 自身的卸载清理已停掉全屏内的定时器。 + useEffect(() => { + return () => { + localStorage.setItem(`${TIME_RANGE_CACHE_KEY}_refresh`, '0'); + }; + }, []); + + return ( + containerRef.current ?? document.body}> +
+
+
+ {title} + + + + + + + LIVE + +
+ + + +
+
{children}
+
+
+ ); +} diff --git a/src/pages/alertCurEvent/pages/List/index.tsx b/src/pages/alertCurEvent/pages/List/index.tsx index 0e8a2c38ad..e4e059e08f 100644 --- a/src/pages/alertCurEvent/pages/List/index.tsx +++ b/src/pages/alertCurEvent/pages/List/index.tsx @@ -1,6 +1,6 @@ import React, { useContext, useState, useMemo, useEffect, useRef, useCallback } from 'react'; -import { Input, Checkbox, Collapse, Segmented, Button, Space } from 'antd'; -import { AlertOutlined, SearchOutlined } from '@ant-design/icons'; +import { Input, Checkbox, Collapse, Segmented, Button, Space, Tooltip } from 'antd'; +import { AlertOutlined, FullscreenOutlined, SearchOutlined } from '@ant-design/icons'; import { useTranslation } from 'react-i18next'; import _ from 'lodash'; import queryString from 'query-string'; @@ -35,6 +35,7 @@ import DatasourceCheckbox from './DatasourceCheckbox'; import AggrRuleDropdown from './AggrRuleDropdown'; import AlertCard, { isEqualEventIds } from './AlertCard'; import AlertTable from './AlertTable'; +import FullscreenList from './FullscreenList'; const AlertCurEvent: React.FC = () => { const { t } = useTranslation(NS); @@ -127,8 +128,28 @@ const AlertCurEvent: React.FC = () => { const [prodFilterExpanded, setProdFilterExpanded] = useState(() => readAlertCurEventSidebarFilterExpanded('prod', true)); const [severityFilterExpanded, setSeverityFilterExpanded] = useState(() => readAlertCurEventSidebarFilterExpanded('severity', true)); const [datasourceFilterExpanded, setDatasourceFilterExpanded] = useState(() => readAlertCurEventSidebarFilterExpanded('datasource', false)); + const fullscreenRef = useRef(null); + const [isFullscreen, setIsFullscreen] = useState(false); const params = getRequestParamsByFilter(filter); + useEffect(() => { + const syncFullscreenState = () => setIsFullscreen(document.fullscreenElement === fullscreenRef.current); + document.addEventListener('fullscreenchange', syncFullscreenState); + return () => document.removeEventListener('fullscreenchange', syncFullscreenState); + }, []); + + const toggleFullscreen = useCallback(async () => { + try { + if (document.fullscreenElement === fullscreenRef.current) { + await document.exitFullscreen(); + } else { + await fullscreenRef.current?.requestFullscreen(); + } + } catch { + // 浏览器可能因权限策略禁止全屏,保持当前页面状态即可。 + } + }, []); + useEffect(() => { if (!IS_PLUS || !getBrainLicense) return; @@ -213,256 +234,279 @@ const AlertCurEvent: React.FC = () => { return ( } title={t('title')} doc='https://flashcat.cloud/docs/content/flashcat-monitor/nightingale-v9/usage/alert-notify/events/cur-events/'> -
-
-
-
-
- +
+ {isFullscreen ? ( + setFilterPatch({ range })} + onRefresh={() => setRefreshFlag(_.uniqueId('refresh_'))} + onExit={toggleFullscreen} + > + + + ) : ( +
+
+
+
- { - setFilterPatch({ my_groups: value }); - localStorage.setItem(MY_GRPUPS_CACHE_KEY, value); + + { + setFilterPatch({ my_groups: value }); + localStorage.setItem(MY_GRPUPS_CACHE_KEY, value); + }} + value={filter.my_groups} + block + options={[ + { label: t('my_groups'), value: 'true' }, + { label: t('all_groups'), value: 'false' }, + ]} + /> + { + setFilterPatch({ bgid: val }); + }} + /> + + } + placeholder={t('search_placeholder')} + value={draftQuery} + onFocus={() => { + queryFocusedRef.current = true; + }} + onBlur={() => { + queryFocusedRef.current = false; + setDraftQuery(filter.query ?? ''); + }} + onChange={(e) => { + const next = e.target.value; + setDraftQuery(next); + commitQueryDebounced(next); }} - value={filter.my_groups} - block + /> + + + {t('tag_display')}: + - { - setFilterPatch({ bgid: val }); + onChange={(value) => { + const next = value as AlertEventTagsDisplayMode; + setTagDisplayMode(next); + writeAlertEventTagsDisplayMode(ALERT_CUR_EVENT_TAGS_EXPANDED_TABLE_KEY, next); }} /> + setFilterPatch({ range })} + onRefresh={() => setRefreshFlag(_.uniqueId('refresh_'))} + localKey={TIME_RANGE_CACHE_KEY} + dateFormat='YYYY-MM-DD HH:mm:ss' + /> + +
-
- {/* 左侧筛选区 */} -
-
- { - const expanded = (Array.isArray(keys) ? keys : [keys]).includes('prod'); - setProdFilterExpanded(expanded); - writeAlertCurEventSidebarFilterExpanded('prod', expanded); - }} - > - - { - setFilterPatch({ rule_prods: val }); - }} - > - {_.map(getProdOptions(feats), (item) => ( -
- - {item.label} -
-
-
- ))} -
-
-
-
-
- { - const expanded = (Array.isArray(keys) ? keys : [keys]).includes('severity'); - setSeverityFilterExpanded(expanded); - writeAlertCurEventSidebarFilterExpanded('severity', expanded); - }} - > - - { - setFilterPatch({ severity: _.map(val, _.toNumber) }); - }} - > - -
- S1(Critical) - -
- -
- S2(Warning) - -
- -
- S3(Info) - -
- - - -
-
- { - const expanded = (Array.isArray(keys) ? keys : [keys]).includes('datasource'); - setDatasourceFilterExpanded(expanded); - writeAlertCurEventSidebarFilterExpanded('datasource', expanded); - }} - > - - { - setFilterPatch({ datasource_ids: val }); - }} - /> - - -
- {/* 右侧内容区 */} -
-
-
-
- -
- +
+ {/* 左侧筛选区 */} +
+
+ { + const expanded = (Array.isArray(keys) ? keys : [keys]).includes('prod'); + setProdFilterExpanded(expanded); + writeAlertCurEventSidebarFilterExpanded('prod', expanded); + }} + > + + { + setFilterPatch({ rule_prods: val }); + }} + > + {_.map(getProdOptions(feats), (item) => ( +
+ + {item.label} +
+
+
+ ))} +
+
+
-
- {selectedRowKeys.length > 0 && ( -
- - {IS_PLUS && alertEscalationEnable && ( - <> - - - - )} + }, + t, + ) + } + > + {t('common:btn.batch_delete')} + + {IS_PLUS && alertEscalationEnable && ( + <> + + + + )} +
+ )} +
+
- )} -
-
-
+ )}
);