// Copyright 2021 99cloud // // 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 // // http://www.apache.org/licenses/LICENSE-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 OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import isEqual from 'react-fast-compare'; import { toJS } from 'mobx'; import { includes, get, isArray, isString } from 'lodash'; import { Button, Table, Dropdown, Input, Typography, Tooltip } from 'antd'; import MagicInput from 'components/MagicInput'; import Pagination from 'components/Pagination'; import { EyeOutlined, SyncOutlined, QuestionCircleOutlined, PlayCircleOutlined, PauseCircleOutlined, FileTextOutlined, } from '@ant-design/icons'; import TimeFilter from 'components/TimeFilter'; import { getColumnSorter, getSortOrder, updateColumnSort, checkIsStatusColumn, getStatusRender, getRender, getValueRenderFunc, getNameRenderByRouter, getNameRender, columnRender, } from 'utils/table'; import { getNoValue } from 'utils/index'; import { getLocalStorageItem, setLocalStorageItem } from 'utils/local-storage'; import { getLinkRender } from 'utils/route-map'; import { inject } from 'mobx-react'; import globalRootStore from 'stores/root'; import CustomColumns from './CustomColumns'; import ItemActionButtons from './ItemActionButtons'; import PrimaryActionButtons from './PrimaryActionButtons'; import BatchActionButtons from './BatchActionButtons'; import Download from './Download'; import styles from './index.less'; export class BaseTable extends React.Component { static propTypes = { data: PropTypes.oneOfType([PropTypes.array, PropTypes.object]).isRequired, columns: PropTypes.array.isRequired, selectedRowKeys: PropTypes.array, isLoading: PropTypes.bool, pagination: PropTypes.object, filters: PropTypes.object, keyword: PropTypes.string, rowKey: PropTypes.any, onFetch: PropTypes.func, onFilterChange: PropTypes.func, onSelectRowKeys: PropTypes.func, getCheckboxProps: PropTypes.func, hideHeader: PropTypes.bool, hideSearch: PropTypes.bool, hideCustom: PropTypes.bool, batchActions: PropTypes.array, alwaysUpdate: PropTypes.bool, emptyText: PropTypes.oneOfType([PropTypes.string || PropTypes.func]), resourceName: PropTypes.string, detailName: PropTypes.string, expandable: PropTypes.object, showTimeFilter: PropTypes.bool, timeFilter: PropTypes.any, isPageByBack: PropTypes.bool, isSortByBack: PropTypes.bool, autoRefresh: PropTypes.bool, hideRefresh: PropTypes.bool, hideAutoRefresh: PropTypes.bool, startRefreshAuto: PropTypes.func, stopRefreshAuto: PropTypes.func, dataDurationAuto: PropTypes.number, defaultSortKey: PropTypes.string, defaultSortOrder: PropTypes.string, hideTotal: PropTypes.bool, hideDownload: PropTypes.bool, primaryActionsExtra: PropTypes.any, isAdminPage: PropTypes.bool, containerProps: PropTypes.any, }; static defaultProps = { rowKey: 'name', selectedRowKeys: [], onFetch() {}, hideHeader: false, hideSearch: false, hideCustom: false, resourceName: '', detailName: '', expandable: undefined, showTimeFilter: false, isPageByBack: false, isSortByBack: false, autoRefresh: true, hideRefresh: false, hideAutoRefresh: false, dataDurationAuto: 15, defaultSortKey: '', defaultSortOrder: '', hideTotal: false, hideDownload: false, primaryActionsExtra: null, isAdminPage: false, }; constructor(props) { super(props); this.state = { hideRow: getLocalStorageItem(`${this.useId}-${this.props.resourceName}`) || [], // eslint-disable-next-line react/no-unused-state filters: [], timeFilter: {}, autoRefresh: props.autoRefresh, }; this.sortKey = props.defaultSortKey; this.sortOrder = props.defaultSortOrder; this.hideableRow = props.columns .filter((column) => !column.hidden) .filter((column) => column.isHideable) .map((column) => ({ label: column.title, value: this.getDataIndex(column.dataIndex) || column.key, })); this.hideableColValues = this.hideableRow.map((item) => item.value); this.suggestions = props.columns .filter((column) => column.search && column.dataIndex) .map((column) => ({ label: column.title, key: column.dataIndex, options: column.filters && column.filters.map((filter) => ({ label: filter.text, key: filter.value, })), })); } get useId() { const { user = {} } = toJS(this.props.rootStore) || {}; const { user: { id } = {} } = user || {}; return id; } get itemActions() { const { itemActions = {} } = this.props; return itemActions; } getDataIndex = (dataIndex) => { if (isArray(dataIndex)) { return dataIndex.join(','); } return dataIndex; }; getSortKey = (sorter) => { const { field, column } = sorter; if (!field) { return null; } if (!column) { return null; } return column.sortKey || column.dataIndex; }; // eslint-disable-next-line no-unused-vars handleChange = (pagination, filters, sorter, extra) => { const { action } = extra; let params = { limit: pagination.pageSize, page: pagination.current, current: pagination.current, sortKey: this.getSortKey(sorter), sortOrder: sorter.order, ...filters, }; const { isCourier, isPageByBack } = this.props; if (action === 'sort') { if (!(isCourier || !isPageByBack)) { const { pagination: propsPagination } = this.props; params = { ...params, limit: propsPagination.pageSize, page: propsPagination.current, current: propsPagination.current, }; } this.sortKey = this.getSortKey(sorter); this.sortOrder = sorter.order; this.props.onFetchBySort(params); } else { this.props.onFetch(params); } }; handlePageChange = (current, pageSize) => { const { filters } = this.state; const { onFetch, defaultSortKey, defaultSortOrder } = this.props; onFetch && onFetch({ limit: pageSize, page: current, current, sortKey: this.sortKey || defaultSortKey, sortOrder: this.sortOrder || defaultSortOrder, ...filters, }); }; handleRefresh = () => { this.props.onRefresh(true); }; handleRowHide = (columns) => { this.setState( { hideRow: this.hideableColValues.filter( (value) => !columns.includes(value) ), }, () => { setLocalStorageItem( `${this.useId}-${this.props.resourceName}`, this.state.hideRow ); } ); }; handleCancelSelect = () => { this.props.onSelectRowKeys([]); }; handleFilterChange = (filters, timeFilter) => { if ( !isEqual(filters, this.props.filters) || !isEqual(timeFilter, this.props.timeFilter) ) { this.setState({ // eslint-disable-next-line react/no-unused-state filters, timeFilter, }); const { pageSize } = this.props.pagination; const { sortKey, sortOrder, onFilterChange } = this.props; onFilterChange && onFilterChange( { limit: pageSize, page: 1, sortKey, sortOrder, ...filters, }, timeFilter ); } }; handleTimeChange = (values) => { this.handleFilterChange(this.state.filters, values); }; handleFilterInput = (tags) => { const filters = {}; tags.forEach((n) => { filters[n.filter.name] = n.value; }); this.handleFilterChange(filters, this.state.timeFilter); }; handleInputFocus = (value) => { const { handleInputFocus } = this.props; handleInputFocus && handleInputFocus(value); }; handleFilterInputText = (e) => { const filters = {}; const { value } = e.currentTarget; if (value) { filters.keywords = value; } this.handleFilterChange(filters, this.state.timeFilter); }; hasItemActions = () => { const { firstAction, moreActions, actionList } = this.itemActions; if (firstAction) { return true; } if (moreActions && moreActions.length) { return true; } return actionList && actionList.length > 0; }; getProjectId = (record) => record.project_id || record.owner || record.fingerprint || record.tenant; getProjectRender = (render) => { if (render) { return render; } return (value, record) => { const projectId = this.getProjectId(record); if (!projectId) { return '-'; } const link = getLinkRender({ key: 'projectDetailAdmin', params: { id: projectId }, value: projectId, }); return ( <>
{globalRootStore.hasAdminRole ? link : projectId}
{value || '-'}
); }; }; getNoValueRender = (render) => { if (render) { return render; } return (value) => getNoValue(value); }; // eslint-disable-next-line no-unused-vars getPriceRender = (render, column) => { if (render) { return render; } return (value) => { const valueStr = isString(value) ? value : (value || 0).toFixed(2); return {valueStr}; }; }; getTipRender = (tip, render, dataIndex, Icon = FileTextOutlined) => { const newRender = (value, record) => { const tipValue = tip(value, record); const realValue = render ? render(value, record) : get(record, dataIndex); if (!tipValue) { return realValue; } return (
{realValue}
); }; return newRender; }; getColumnTitle = (column) => { const { title, titleTip } = column; if (!titleTip) { return title; } return ( {title} ); }; getBaseColumns = (columns) => columns.map((column) => { const { Paragraph } = Typography; const { sortable, dataIndex, valueRender, sorter, sortOrder, render, copyable, tip, isStatus, isName, isLink, routeName, linkPrefix, isPrice, ...rest } = column; const newSorter = getColumnSorter(column, this.props); const newSortOrder = sortOrder || newSorter ? getSortOrder(dataIndex, this.props) : null; let newRender = render || getRender(valueRender); if (checkIsStatusColumn(dataIndex, isStatus)) { newRender = getStatusRender(newRender); } if (dataIndex === 'description') { newRender = this.getNoValueRender(newRender); } if (dataIndex === 'project_name') { newRender = this.getProjectRender(newRender); } if ((dataIndex === 'name' && routeName) || isLink) { const { rowKey } = this.props; newRender = getNameRenderByRouter(newRender, column, rowKey); } if ((dataIndex === 'name' && linkPrefix) || isName) { newRender = getNameRender(newRender, column); } if (dataIndex === 'cost' || isPrice) { newRender = this.getPriceRender(newRender, column); } if (copyable) { newRender = (value) => { if (value && value !== '-') { return {value}; } return '-'; }; } if (tip) { const { tipIcon } = column; newRender = this.getTipRender(tip, newRender, dataIndex, tipIcon); } const newColumn = { ...rest, title: this.getColumnTitle(column), dataIndex, align: column.align || 'left', }; if (newSorter) { newColumn.sorter = newSorter; } if (sortOrder) { newColumn.sortOrder = newSortOrder; } updateColumnSort(newColumn, this.props); if (newRender) { newColumn.render = newRender; } return { ...newColumn, render: (value, record) => columnRender(newColumn.render, value, record), }; }); getColumns = () => { const { columns, containerProps, onClickAction, onFinishAction, onCancelAction, isAdminPage, } = this.props; const { hideRow } = this.state; const currentColumns = columns .filter((it) => !it.hidden) .filter((it) => !includes(hideRow, this.getDataIndex(it.dataIndex))); const baseColumns = this.getBaseColumns(currentColumns); if (!this.hasItemActions()) { return baseColumns; } return [ ...baseColumns, { title: t('Action'), key: 'operation', width: 150, render: (text, record, index) => ( ), }, ]; }; stopRefreshAuto = () => { this.setState({ autoRefresh: false, }); const { stopRefreshAuto } = this.props; if (stopRefreshAuto) { stopRefreshAuto(); } }; startRefreshAuto = () => { this.setState({ autoRefresh: true, }); const { startRefreshAuto } = this.props; if (startRefreshAuto) { startRefreshAuto(); } }; filterDownloadColumns(columns) { const { rowKey } = this.props; const downloadColumns = columns .filter((it) => !it.hidden) .map((it) => { const { title, splitColumnForDownload = true } = it; if (title.includes('/') && splitColumnForDownload) { const [fTitle, sTitle] = it.title.split('/'); let sName = sTitle; if (fTitle.length > 2) { sName = `${fTitle.split('ID')[0]}${sTitle}`; } let fIndex = it.idKey || rowKey; if ( it.title.includes(t('Project')) && it.dataIndex === 'project_name' ) { fIndex = 'project_id'; } return [ { title: fTitle, dataIndex: fIndex, }, { ...it, title: sName, }, ]; } return it; }); return [].concat(...downloadColumns); } renderBatchActions() { const { batchActions, selectedRowKeys, data, rowKey, containerProps, onClickAction, onFinishAction, onCancelAction, resourceName, isAdminPage, } = this.props; const selectedItems = data.filter( (it) => selectedRowKeys.indexOf(it[rowKey]) >= 0 ); if (batchActions) { return ( ); } return null; } renderSelectedTitle = () => (
{this.renderBatchActions()}
); renderTimeFilter() { const { showTimeFilter, filterTimeDefaultValue } = this.props; if (!showTimeFilter) { return null; } const props = { onChange: this.handleTimeChange, className: styles.timer, }; if (filterTimeDefaultValue !== undefined) { props.defaultValue = filterTimeDefaultValue; } return ; } renderSearch() { const { hideSearch, searchFilters, initFilter = {} } = this.props; if (hideSearch) { return null; } if (searchFilters.length > 0) { return (
); } return (
); } renderActions() { const { isAdminPage, primaryActions, containerProps, onClickAction, onFinishAction, onCancelAction, primaryActionsExtra, } = this.props; if (primaryActions) { return ( ); } return null; } renderCustomButton() { const { hideCustom } = this.props; if (hideCustom) { return null; } return (