diff --git a/web/console/src/modules/alarmRecord/components/AlarmRecordApp.tsx b/web/console/src/modules/alarmRecord/components/AlarmRecordApp.tsx index 54646a369..4a5512091 100644 --- a/web/console/src/modules/alarmRecord/components/AlarmRecordApp.tsx +++ b/web/console/src/modules/alarmRecord/components/AlarmRecordApp.tsx @@ -25,8 +25,8 @@ import { allActions } from '../actions'; import { RootState } from '../models'; import { router } from '../router'; import { configStore } from '../stores/RootStore'; -import { AlarmRecordPanel } from './AlarmRecordPanel'; import { AlarmRecordHeadPanel } from './AlarmHeaderPanel'; +import { AlarmTablePanel } from './AlarmTablePanel'; const { useState, useEffect } = React; const { Body, Content } = Layout; @@ -63,8 +63,8 @@ class AlarmRecordApp extends React.Component<RootProps, {}> { <Content.Header title={t('历史告警记录')}> <AlarmRecordHeadPanel /> </Content.Header> - <Content.Body> - <AlarmRecordPanel /> + <Content.Body full> + <AlarmTablePanel clusterId={this?.props?.cluster?.selection?.metadata?.name} /> </Content.Body> </Content> </Body> diff --git a/web/console/src/modules/alarmRecord/components/AlarmRecordPanel.tsx b/web/console/src/modules/alarmRecord/components/AlarmRecordPanel.tsx deleted file mode 100644 index f844266c7..000000000 --- a/web/console/src/modules/alarmRecord/components/AlarmRecordPanel.tsx +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Tencent is pleased to support the open source community by making TKEStack - * available. - * - * Copyright (C) 2012-2021 Tencent. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use - * this file except in compliance with the License. You may obtain a copy of the - * License at - * - * https://opensource.org/licenses/Apache-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ -import * as React from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { Text, TableColumn, Table, Justify, SearchBox, Bubble } from '@tea/component'; -import { TablePanel } from '@tencent/ff-component'; -import { bindActionCreators, insertCSS } from '@tencent/ff-redux'; -import { t, Trans } from '@tencent/tea-app/lib/i18n'; -import { useModal } from '../../common/utils'; -import { emptyTips, LinkButton } from '../../common/components'; -import { allActions } from '../actions'; -import { AlarmRecord } from '../models'; -import { dateFormatter } from '@helper/dateFormatter'; -import { useRafInterval } from 'ahooks'; - -export const AlarmRecordPanel = () => { - const state = useSelector(state => state); - const dispatch = useDispatch(); - const { actions } = bindActionCreators({ actions: allActions }, dispatch); - const { alarmRecord, route } = state; - const selectedClusterId = route.queries.clusterId; - - useRafInterval( - () => { - if (selectedClusterId) { - actions.alarmRecord.applyFilter({ clusterID: selectedClusterId }); - } else { - actions.alarmRecord.clear(); - } - }, - 5000, - { immediate: true } - ); - - const formatManager = managers => { - if (managers) { - return managers.map((m, index) => { - return ( - <p key={index} className="text-overflow"> - {m} - </p> - ); - }); - } - }; - - const columns: TableColumn<AlarmRecord>[] = [ - { - key: 'metadata.creationTimestamp', - header: t('发生时间'), - render: item => ( - <Text parent="div">{dateFormatter(new Date(item.metadata.creationTimestamp), 'YYYY-MM-DD HH:mm:ss')}</Text> - ) - }, - { - key: 'spec.alarmPolicyName', - header: t('告警策略'), - render: item => <Text parent="div">{item.spec.alarmPolicyName || '-'}</Text> - }, - { - key: 'spec.alarmPolicyType', - header: t('策略类型'), - render: item => <Text parent="div">{item.spec.alarmPolicyType || '-'}</Text> - }, - { - key: 'spec.body', - header: t('告警内容'), - width: 400, - render: item => { - const content = item.spec.body; - const showContent = content.length >= 250 ? content.substr(0, 250) + '...' : content; - return ( - <Bubble placement="left" content={content || null}> - <Text parent="div">{showContent || '-'}</Text> - </Bubble> - ); - } - }, - { - key: 'spec.receiverChannelName', - header: t('通知渠道'), - render: item => <Text parent="div">{item.spec.receiverChannelName || '-'}</Text> - }, - { - key: 'spec.receiverName', - header: t('接受组'), - render: item => { - const members = item.spec.receiverName ? item.spec.receiverName.split(',') : []; - return ( - <Bubble placement="left" content={formatManager(members) || null}> - <span className="text"> - {formatManager(members ? members.slice(0, 1) : [])} - <Text parent="div" overflow> - {members && members.length > 1 ? '...' : ''} - </Text> - </span> - </Bubble> - ); - } - } - ]; - - return ( - <> - <Table.ActionPanel> - <Justify - left={<React.Fragment />} - right={ - <SearchBox - value={alarmRecord.query.keyword || ''} - onChange={actions.alarmRecord.changeKeyword} - onSearch={actions.alarmRecord.performSearch} - onClear={() => { - actions.alarmRecord.performSearch(''); - }} - placeholder={t('请输入告警策略名')} - /> - } - /> - </Table.ActionPanel> - <TablePanel - recordKey={record => { - return record.id; - }} - columns={columns} - model={alarmRecord} - action={actions.alarmRecord} - rowDisabled={record => record.status['phase'] === 'Terminating'} - emptyTips={emptyTips} - isNeedContinuePagination={true} - bodyClassName={'tc-15-table-panel tc-15-table-fixed-body'} - /> - </> - ); -}; diff --git a/web/console/src/modules/alarmRecord/components/AlarmTablePanel.tsx b/web/console/src/modules/alarmRecord/components/AlarmTablePanel.tsx new file mode 100644 index 000000000..d058c49e5 --- /dev/null +++ b/web/console/src/modules/alarmRecord/components/AlarmTablePanel.tsx @@ -0,0 +1,209 @@ +import React, { useState } from 'react'; +import { Justify, Table, TableColumn, Text, Bubble, Pagination, TagSearchBox } from 'tea-component'; +import { useFetch } from '@src/modules/common/hooks/useFetch'; +import { fetchAlarmList } from '@src/webApi/alarm'; +import { t } from '@/tencent/tea-app/lib/i18n'; +import { dateFormatter } from '@helper/dateFormatter'; + +const { filterable, autotip } = Table?.addons; +const ALL_VALUE = ''; + +const defaultPageSize = 10; + +export const AlarmTablePanel = ({ clusterId }) => { + const columns: TableColumn[] = [ + { + key: 'metadata.creationTimestamp', + header: t('发生时间'), + render: item => <Text>{dateFormatter(new Date(item.metadata.creationTimestamp), 'YYYY-MM-DD HH:mm:ss')}</Text> + }, + + { + key: 'spec.alarmPolicyName', + header: t('告警策略'), + render: item => <Text copyable>{item.spec.alarmPolicyName || '-'}</Text> + }, + + { + key: 'spec.alarmPolicyType', + header: t('策略类型'), + render: item => <Text>{item.spec.alarmPolicyType || '-'}</Text> + }, + + { + key: 'spec.body', + header: t('告警内容'), + width: 400, + render: item => { + const content = item.spec.body; + const showContent = content.length >= 250 ? content.substr(0, 250) + '...' : content; + return ( + <Bubble placement="left" content={content || null}> + <Text>{showContent || '-'}</Text> + </Bubble> + ); + } + }, + + { + key: 'status.alertStatus', + header: '告警状态', + render: item => ( + <Text theme={item?.status?.alertStatus === 'resolved' ? 'success' : 'danger'}> + {item?.status?.alertStatus === 'resolved' ? '已恢复' : '未恢复'} + </Text> + ) + }, + + { + key: 'spec.receiverChannelName', + header: t('通知渠道'), + render: item => <Text copyable>{item.spec.receiverChannelName || '-'}</Text> + }, + + { + key: 'spec.receiverName', + header: t('接收人'), + render: item => { + return ( + <Text overflow copyable> + {item?.spec?.receiverName ?? '-'} + </Text> + ); + } + } + ]; + + const [query, setQuery] = useState({}); + + const [alertStatus, setAlertStatus] = useState(ALL_VALUE); + + const { + data: alarmList, + paging, + status + } = useFetch( + async ({ paging, continueToken }) => { + const rsp = await fetchAlarmList({ clusterId }, { limit: paging?.pageSize, continueToken, query, alertStatus }); + + return { + data: rsp?.items ?? [], + continueToken: rsp?.metadata?.continue ?? null, + totalCount: null + }; + }, + [clusterId, query, alertStatus], + { + mode: 'continue', + fetchAble: !!clusterId, + polling: true, + pollingDelay: 5 * 1000, + needClearData: false, + defaultPageSize, + onlyPollingPage1: true + } + ); + + return ( + <> + <Table.ActionPanel> + <Justify + right={ + <TagSearchBox + hideHelp + minWidth={360} + style={{ maxWidth: 640 }} + attributes={[ + { + type: 'input', + key: 'spec.alarmPolicyName', + name: t('策略名称') + }, + + { + type: 'single', + key: 'spec.alarmPolicyType', + name: t('策略类型'), + values: [ + { key: 'cluster', name: '集群' }, + { key: 'node', name: '节点' }, + { key: 'pod', name: 'Pod' }, + { key: 'virtualMachine', name: '虚拟机' } + ] + }, + + { + type: 'input', + key: 'spec.receiverChannelName', + name: t('通知渠道') + }, + + { + type: 'input', + key: 'spec.receiverName', + name: t('接收人') + } + ]} + onSearchButtonClick={(_, tags) => { + const query = tags.reduce((all, tag) => { + const value = tag?.values?.[0]; + + return { + ...all, + [tag?.attr?.key]: value?.key ?? value?.name + }; + }, {}); + + setQuery(query); + }} + onClearButtonClick={() => setQuery({})} + /> + } + /> + </Table.ActionPanel> + + <Table + columns={columns} + records={alarmList} + addons={[ + filterable({ + type: 'single', + column: 'status.alertStatus', + value: alertStatus, + onChange: value => setAlertStatus(value), + // 增加 "全部" 选项 + all: { + value: ALL_VALUE, + text: '全部' + }, + // 选项列表 + options: [ + { value: 'firing', text: '未恢复' }, + { value: 'resolved', text: '已恢复' } + ] + }), + + autotip({ + isLoading: status === 'loading', + isError: status === 'error', + emptyText: '暂无数据' + }) + ]} + /> + + <Pagination + recordCount={paging?.totalCount ?? 0} + stateText={<Text>{`第${paging.pageIndex}页`}</Text>} + pageIndexVisible={false} + endJumpVisible={false} + pageSize={defaultPageSize} + pageSizeVisible={false} + onPagingChange={({ pageIndex }) => { + if (pageIndex > paging.pageIndex) paging.nextPageIndex(); + + if (pageIndex < paging.pageIndex) paging.prePageIndex(); + }} + /> + </> + ); +}; diff --git a/web/console/src/modules/cluster/actions/namespaceActions.ts b/web/console/src/modules/cluster/actions/namespaceActions.ts index 97cd4da97..b6f2930d8 100644 --- a/web/console/src/modules/cluster/actions/namespaceActions.ts +++ b/web/console/src/modules/cluster/actions/namespaceActions.ts @@ -79,7 +79,11 @@ const restActions = { } // 初始化或者变更Resource的信息,在创建页面当中,变更ns,不需要拉取resource - mode !== 'create' && dispatch(resourceActions.poll()); + + if (mode !== 'create') { + dispatch(resourceActions.resetPaging()); + dispatch(resourceActions.poll()); + } }; } }; diff --git a/web/console/src/modules/cluster/components/resource/ResourceContainerPanel.tsx b/web/console/src/modules/cluster/components/resource/ResourceContainerPanel.tsx index 2e455eb0b..51e173ede 100644 --- a/web/console/src/modules/cluster/components/resource/ResourceContainerPanel.tsx +++ b/web/console/src/modules/cluster/components/resource/ResourceContainerPanel.tsx @@ -98,7 +98,11 @@ export class ResourceContainerPanel extends React.Component<RootProps, ResourceC if (newMode !== '' && oldMode !== newMode && newMode !== mode) { actions.resource.selectMode(newMode); // 这里是判断回退动作,取消动作等的时候,回到list页面,需要重新拉取一下,激活一下轮训的状态等 - newUrlParam['sub'] === 'sub' && !isEmpty(resourceInfo) && newMode === 'list' && actions.resource.poll(); + if (newUrlParam['sub'] === 'sub' && !isEmpty(resourceInfo) && newMode === 'list') { + actions.resource.resetPaging(); + actions.resource.poll(); + } + // newUrlParam['sub'] === 'sub' && !isEmpty(resourceInfo) && newMode === 'list' && newResourceName !== 'hpa' && actions.resource.poll(); } diff --git a/web/console/src/modules/common/hooks/useFetch.ts b/web/console/src/modules/common/hooks/useFetch.ts index d30829467..827f8dc03 100644 --- a/web/console/src/modules/common/hooks/useFetch.ts +++ b/web/console/src/modules/common/hooks/useFetch.ts @@ -28,6 +28,7 @@ interface IUseFetchOptions<T> { polling?: boolean; pollingDelay?: number; needClearData?: boolean; + onlyPollingPage1?: boolean; } type IUseFetchQuery<T> = (params?: IQueryParams) => Promise<IQueryResponse<T>>; @@ -48,7 +49,8 @@ export function useFetch<T>( fetchAble = true, polling = false, pollingDelay = 5000, - needClearData = true + needClearData = true, + onlyPollingPage1 = false } = options ?? {}; const [data, _setData] = useState<T>(null); @@ -65,24 +67,6 @@ export function useFetch<T>( _setFlag(pre => pre + 1); } - // 定时相关 - const timer = useRef(null); - useEffect(() => { - clearInterval(timer.current); - - const _timer = setInterval(() => { - if (!polling) return; - - if (status === 'loading' || status === 'loading-polling') return; - - fetchData(true); - }, pollingDelay); - - timer.current = _timer; - - return () => clearInterval(timer.current); - }, [polling, status, pollingDelay]); - // 普通翻页相关的 const [totalCount, setTotalCount] = useState<number>(null); const [pageIndex, _setPageIndex] = useState(1); @@ -100,6 +84,26 @@ export function useFetch<T>( reFetch(); } + // 定时相关 + const timer = useRef(null); + useEffect(() => { + clearInterval(timer.current); + + const _timer = setInterval(() => { + if (!polling) return; + + if (status === 'loading' || status === 'loading-polling') return; + + if (onlyPollingPage1 && pageIndex !== 1) return; + + fetchData(true); + }, pollingDelay); + + timer.current = _timer; + + return () => clearInterval(timer.current); + }, [polling, status, pollingDelay, onlyPollingPage1, pageIndex]); + // continue分页相关的 const [continueState, setContinueState] = useState([null]); @@ -137,9 +141,16 @@ export function useFetch<T>( } case 'continue': { - const pageIndex = paging.pageIndex; + const { pageIndex, pageSize } = paging; const currentContinue = continueState[pageIndex - 1]; - const { data, continueToken, totalCount } = await query({ paging, continueToken: currentContinue }); + let { data, continueToken, totalCount } = await query({ paging, continueToken: currentContinue }); + + // 针对不返回totalcount的情况 + + if (totalCount === null) { + totalCount = (pageIndex + (continueToken ? 1 : 0)) * pageSize; + } + setContinueState(pre => { const newState = [...pre]; diff --git a/web/console/src/webApi/alarm.ts b/web/console/src/webApi/alarm.ts new file mode 100644 index 000000000..c5e8a37ab --- /dev/null +++ b/web/console/src/webApi/alarm.ts @@ -0,0 +1,23 @@ +import { Request, generateQueryString } from './request'; + +export function fetchAlarmList({ clusterId }, { limit = null, continueToken = null, query = {}, alertStatus = null }) { + return Request.get<any, any>( + `/apis/notify.tkestack.io/v1/messages?${generateQueryString({ + limit, + continue: continueToken, + fieldSelector: generateQueryString( + { + ...query, + 'spec.clusterID': clusterId, + 'status.alertStatus': alertStatus + }, + ',' + ) + })}`, + { + headers: { + 'X-TKE-ClusterName': clusterId + } + } + ); +} diff --git a/web/console/src/webApi/request.ts b/web/console/src/webApi/request.ts index e81041246..865e9c346 100644 --- a/web/console/src/webApi/request.ts +++ b/web/console/src/webApi/request.ts @@ -69,9 +69,9 @@ export default instance; export const Request = instance; -export const generateQueryString = (query: Record<string, any>) => { +export const generateQueryString = (query: Record<string, any>, joinKey = '&') => { return Object.entries(query) .filter(([_, value]) => value !== undefined && value !== null && value !== '') .map(([key, value]) => `${key}=${value}`) - .join('&'); + .join(joinKey); };