// 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 { parse } from 'qs'; import classnames from 'classnames'; import { toJS } from 'mobx'; import { isEmpty, isFunction, get, isString, isEqual, isArray, has, } from 'lodash'; import { Menu, Icon, Dropdown, Button, Alert } from 'antd'; import BaseTable from 'components/Tables/Base'; import { isAdminPage } from 'utils/index'; import Notify from 'components/Notify'; import { checkTimeIn } from 'utils/time'; import checkItemPolicy from 'resources/policy'; import NotFound from 'components/Cards/NotFound'; import styles from './index.less'; const tabOtherHeight = 326; const otherHeight = 272; const hintHeight = 50; const subTabHeight = 50; export default class BaseList extends React.Component { constructor(props, options = {}) { super(props); this.options = options; this.state = { filters: {}, timeFilter: {}, autoRefresh: true, newHints: false, }; this.dataTimerTransition = null; this.dataTimerAuto = null; this.dataDurationTransition = 10; // fresh data interval when item in transition this.dataDurationAuto = 30; // fresh data interval auto this.autoRefreshTotalTime = 60 * 10; // seconds this.autoRefreshCount = 0; // operation will reset the count, such as: action, select, pagination this.autoRefreshCountMax = Math.floor( this.autoRefreshTotalTime / this.dataDurationAuto ); this.infoMessage = ''; this.successMessage = ''; this.errorMessage = ''; this.warnMessage = ''; this.inAction = false; this.inSelect = false; this.init(); } componentDidMount() { this.unsubscribe = this.routing.history.subscribe((location) => { if ( location.pathname === this.props.match.url && location.key === this.props.location.key ) { const params = parse(location.search.slice(1)); // this.getDataWithPolicy(params); const { limit, page } = this.store.list; this.list.filters = {}; this.handleFetch({ ...params, limit, page }, true); } }); } componentWillUnmount() { this.unsubscribe && this.unsubscribe(); this.disposer && this.disposer(); this.unMountActions && this.unMountActions(); this.stopRefreshTransition(); this.stopRefreshAuto(); if (this.clearListUnmount) { this.store.clearData && this.store.clearData('listUnmount'); } } get policy() { return ''; } get name() { return ''; } get title() { return `${this.name}s`; } get className() { return ''; } get isInDetailPage() { const { detail } = this.props; return !!detail; } get alsoRefreshDetail() { return true; } get location() { return this.props.location; } get isAdminPage() { const { pathname } = this.location; return isAdminPage(pathname); } get hasAdminRole() { return this.props.rootStore.hasAdminRole; } getUrl(path, adminStr) { return this.isAdminPage ? `${path}${adminStr || '-admin'}` : path; } get prefix() { return this.props.match.url; } get params() { return this.props.match.params || {}; } get routing() { return this.props.rootStore.routing; } get list() { return this.store.list; } get isLoading() { return this.list.isLoading || this.store.isSubmitting; } get tips() { return []; } get rowKey() { return 'id'; } get hasTab() { return false; } get hasSubTab() { return false; } get hideCustom() { return false; } get hideSearch() { return false; } get hideRefresh() { return false; } get hideDownload() { return false; } get checkEndpoint() { return false; } get endpoint() { return ''; } get endpointError() { return this.checkEndpoint && !this.endpoint; } get hintHeight() { let height = 0; if (this.infoMessage) { height += hintHeight; } if (this.warnMessage) { height += hintHeight; } if (this.errorMessage) { height += hintHeight; } if (this.successMessage) { height += hintHeight; } return height; } get tableTopHeight() { if (this.hasSubTab) { return tabOtherHeight + subTabHeight; } if (this.hasTab) { return tabOtherHeight; } return otherHeight; } get tableHeight() { const height = window.innerHeight; const id = this.params && this.params.id; if (id) { return -1; } return height - this.tableTopHeight - this.hintHeight; } get tableWidth() { return 800; } get isFilterByBackend() { return false; } get isSortByBackend() { return false; } get isCourier() { // 如果是courier的并且是后端分页的,那么需要带数字的分页脚标,因为Courier自带分页且能跳页,而openstack的不支持跳页。 return false; } get enabledItemActions() { return this.itemActions.filter((item) => !item.action); } get adminPageHasProjectFilter() { return false; } get transitionStatusList() { return []; } get fetchDataByAllProjects() { return true; } get currentUser() { const { user } = this.props.rootStore || {}; return user || {}; } get currentProjectId() { return this.props.rootStore.projectId; } get fetchDataByCurrentProject() { // add project_id to fetch data; return false; } get defaultSortKey() { return ''; } get defaultSortOrder() { return 'descend'; } get clearListUnmount() { return false; } get itemInTransitionFunction() { return (item) => { const { status } = item; return this.transitionStatusList.indexOf(status) >= 0; }; } get ableAutoFresh() { return true; } get actionConfigs() { return { batchActions: [], primaryActions: [], rowActions: [], }; } get primaryActions() { return this.actionConfigs.primaryActions; } get batchActions() { return this.actionConfigs.batchActions; } get itemActions() { return this.actionConfigs.rowActions; } get searchFilters() { return []; } get expandable() { return undefined; } get filterTimeKey() { return undefined; } get projectFilterKey() { return 'project_id'; } get pageSizeOptions() { return undefined; } get hideTotal() { return false; } get primaryActionsExtra() { return null; } get allProjectsKey() { return 'all_projects'; } setRefreshdataTimerTransition = () => { this.stopRefreshAuto(); if (this.dataTimerTransition) { return; } this.dataTimerTransition = setTimeout(() => { this.handleRefresh(); this.dataTimerTransition = null; }, this.dataDurationTransition * 1000); }; setRefreshdataTimerAuto = () => { this.stopRefreshTransition(); if (!this.ableAutoFresh) { return; } const { autoRefresh } = this.state; if (!autoRefresh || this.dataTimerAuto) { return; } this.dataTimerAuto = setTimeout(() => { this.autoRefreshCount += 1; this.handleRefresh(); this.dataTimerAuto = null; }, this.dataDurationAuto * 1000); }; getEmptyProps() { return {}; } getEnabledTableProps() { const props = this.getTableProps(); if (isEmpty(this.batchActions)) { props.onSelectRowKeys = null; } return props; } getCheckboxProps(record) { return { disabled: false, name: record.name, }; } onStopRefreshAuto = () => { this.setState({ autoRefresh: false, }); this.stopRefreshAuto(); }; onClickAction = () => { this.inAction = true; this.autoRefreshCount = 0; }; onFinishAction = () => { this.inAction = false; this.handleSelectRowKeys([]); this.handleRefresh(true); }; onCancelAction = () => { this.inAction = false; this.getDataSource(); }; handleInputFocus = (value) => { this.inAction = value; if (!value) { this.setRefreshdataTimerAuto(); } }; getTableProps() { return { onRefresh: this.handleRefresh, onFetch: this.handleFetch, onFetchBySort: this.handleFetchBySort, onSelectRowKeys: this.handleSelectRowKeys, onFilterChange: this.handleFilterChange, hideCustom: this.hideCustom, hideSearch: this.hideSearch, hideRefresh: this.hideRefresh, hideAutoRefresh: !this.ableAutoFresh, }; } getData({ silent, ...params } = {}) { silent && (this.list.silent = true); const newParams = { ...this.props.match.params, ...params, sortKey: params.sortKey || (this.isSortByBackend && this.defaultSortKey) || '', sortOrder: params.sortOrder || (this.isSortByBackend && this.defaultSortOrder) || '', }; if (!this.isAdminPage && this.fetchDataByCurrentProject) { newParams.project_id = this.currentProjectId; } else if ( this.isAdminPage && this.fetchDataByAllProjects && this.allProjectsKey ) { newParams[this.allProjectsKey] = true; } if (this.isFilterByBackend) { this.fetchListWithTry(() => this.fetchDataByPage(this.updateFetchParamsByPage(newParams)) ); } else { this.fetchListWithTry(() => this.fetchData(this.updateFetchParams(newParams)) ); } } getDataWithPolicy(params) { if (!this.currentUser || isEmpty(this.currentUser)) { return; } if (this.endpointError) { return; } if (!checkItemPolicy({ policy: this.policy, actionName: this.name })) { const error = { message: t("You don't have access to get {name}.", { name: this.name.toLowerCase(), }), status: 401, }; Notify.errorWithDetail( error, t('Unable to get {name}.', { name: this.name.toLowerCase() }) ); this.list.isLoading = false; this.list.silent = false; return; } this.getData(params); } fetchListWithTry = async (func) => { try { func && (await func()); } catch (e) { // eslint-disable-next-line no-console console.log('fetch list error', e); const { message = '', data, status } = (e || {}).response || e || {}; if (status === 500) { const sysErr = t('System is error, please try again later.'); const title = `${t('Get {name} error.', { name: this.name.toLowerCase(), })} ${sysErr}`; Notify.errorWithDetail(null, title); } else { const error = { message: data || message || e || '', status, }; Notify.errorWithDetail( error, t('Get {name} error.', { name: this.name.toLowerCase() }) ); } this.list.isLoading = false; this.list.silent = false; } }; updateFetchParamsByPage = (params) => params; updateFetchParams = (params) => params; fetchDataByPage = async (params) => { await this.store.fetchListByPage(params); this.list.silent = false; }; fetchData = async (newParams) => { await this.store.fetchList(newParams); this.list.silent = false; }; fetchDownloadData = async (params) => { let result = []; if (this.isFilterByBackend) { result = await this.downloadStore.fetchListByPage( this.updateFetchParamsByPage(params) ); } else { result = await this.downloadStore.fetchList( this.updateFetchParams(params) ); } return result; }; getDownloadData = async ({ ...params } = {}) => { // only used for donwload all and pagination by backend const { filters } = this.state; const newParams = { ...this.props.match.params, ...params, ...filters, sortKey: params.sortKey || (this.isSortByBackend && this.defaultSortKey) || '', sortOrder: params.sortOrder || (this.isSortByBackend && this.defaultSortOrder) || '', }; if (!this.isAdminPage && this.fetchDataByCurrentProject) { newParams.project_id = this.currentProjectId; } else if ( this.isAdminPage && this.fetchDataByAllProjects && this.allProjectsKey ) { newParams[this.allProjectsKey] = true; } const result = await this.fetchDownloadData(newParams); return result; }; startRefreshAuto = () => { this.autoRefreshCount = 0; this.setState({ autoRefresh: true, }); this.handleRefresh(); }; stopRefreshAuto = () => { clearTimeout(this.dataTimerAuto); this.dataTimerAuto = null; }; stopRefreshTransition = () => { clearTimeout(this.dataTimerTransition); this.dataTimerTransition = null; }; getFilteredValue = (dataIndex) => this.list.filters[dataIndex]; getColumns = () => []; checkIsProjectFilter = (item) => item.name === this.projectFilterKey; getSearchFilters = () => { const filters = this.searchFilters; if (!this.isAdminPage) { return filters; } if (!this.adminPageHasProjectFilter) { return filters; } const projectItem = filters.find((it) => this.checkIsProjectFilter(it)); if (projectItem) { return filters; } return [ ...filters, { label: t('Project ID'), name: this.projectFilterKey, }, ]; }; filterDataByTime = (data) => { if (!this.filterTimeKey) { return true; } const { timeFilter: { value = 0, start, end } = {} } = this.state; if (value === 0) { return true; } const dataTime = get(data, this.filterTimeKey, 0); if (value !== 1) { return checkTimeIn(dataTime, new Date().getTime() - value, null); } return checkTimeIn(dataTime, start, end); }; checkFilterInclude = (key) => { const item = this.searchFilters.find((it) => it.name === key); if (has(item, 'include')) { return item.include; } if (has(item, 'options')) { return false; } return true; }; filterData = (data) => { if (!this.filterDataByTime(data)) { return false; } const { filters: filtersInState } = this.state; if (Object.keys(filtersInState).length === 1 && filtersInState.keywords) { const { keywords } = filtersInState; const item = Object.values(data).find( (value) => (isString(value) || isArray(value)) && value.indexOf(keywords) >= 0 ); return !!item; } const failed = Object.keys(filtersInState).find((key) => { const value = get(data, key); const filterValue = filtersInState[key]; const { filterFunc } = this.searchFilters.find((i) => i.name === key); if (filterFunc) { return !filterFunc(value, filterValue); } const isInclude = this.checkFilterInclude(key); if (isString(value) && isString(filterValue)) { if (isInclude) { return value.toLowerCase().indexOf(filterValue.toLowerCase()) < 0; } return value.toLowerCase() !== filterValue.toLowerCase(); } return !isEqual(value, filterValue); }); return !failed; }; getDataSource = () => { const { data, filters = {} } = this.list; const { timeFilter = {} } = this.state; const { id, tab, ...rest } = filters; const newFilters = rest; let datas = []; if (this.isFilterByBackend) { datas = toJS(data); } else { datas = (toJS(data) || []).filter((it) => this.filterData(it, toJS(newFilters), toJS(timeFilter)) ); this.updateList({ total: datas.length }); } const hasTransData = datas.some((item) => this.itemInTransitionFunction(item) ); if (hasTransData) { this.setRefreshdataTimerTransition(); } else { this.setRefreshdataTimerAuto(); } this.updateHintsByDatas(datas); return datas; }; getFilters = () => { const { filters } = this.list; const params = parse(this.location.search.slice(1)); return { ...params, ...toJS(filters), }; }; handleMoreMenuClick = (item) => (e, key) => { const action = this.enabledItemActions.find( (_action) => _action.key === key ); if (action && action.onClick) { action.onClick(item); } }; refreshDetailData = () => { const { refreshDetail } = this.props; refreshDetail && refreshDetail(); }; handleRefresh = (force) => { const { inAction, inSelect } = this; if (inAction || (inSelect && !force)) { return; } if (!force && this.autoRefreshCount >= this.autoRefreshCountMax) { return; } if (force) { this.autoRefreshCount = 0; } const { page, limit, sortKey, sortOrder, filters } = this.list; const params = { page, limit, sortKey, sortOrder, ...toJS(filters), silent: !force, }; this.handleFetch(params, true); if (this.isInDetailPage && force && this.alsoRefreshDetail) { this.refreshDetailData(); } }; updateList = (newObj) => { if (!this.list) { return; } if (this.list.update) { this.list.update(newObj); } else { Object.keys(newObj).forEach((key) => { this.list[key] = newObj[key]; }); } }; handleFetch = (params, refresh) => { if (refresh && !this.isFilterByBackend) { this.getDataWithPolicy(params); return; } // eslint-disable-next-line no-unused-vars const { sortKey, limit, page, current, sortOrder, ...rest } = params; if (page !== this.list.page || limit !== this.list.limit) { this.autoRefreshCount = 0; } // const newParams = reverse === undefined ? rest : params; if (this.isFilterByBackend) { this.getDataWithPolicy({ ...params, ...(this.list.filters || {}) }); // this.routing.query(newParams, refresh); } else { this.updateList({ page, limit, sortKey, sortOrder, }); } }; handleFetchBySort = (params) => { if (!this.isSortByBackend) { const { sortKey, limit, page, sortOrder } = params; this.updateList({ page, limit, sortKey, sortOrder, }); return; } const newParams = { ...params, page: 1, }; this.handleFetch(newParams, true); }; handleFilterChange = (filters, timeFilter) => { const { page, limit, sortKey, sortOrder, ...rest } = filters; if (this.isFilterByBackend) { this.list.filters = filters; // this.list.timeFilter = timeFilter; this.setState( { timeFilter, }, () => { this.handleFetch(filters, true); } ); // this.routing.query(filters, true); } else { this.updateList({ page, sortKey, sortOrder, filters: rest, // timeFilter, }); this.setState({ filters: rest, timeFilter, }); } }; handleSelectRowKeys = (params) => { this.store.setSelectRowKeys('list', params); if (!params || params.length === 0) { this.inSelect = false; this.getDataSource(); } else { this.inSelect = true; this.autoRefreshCount = 0; } }; onCloseSuccessHint = () => {}; // eslint-disable-next-line no-unused-vars updateHintsByOthers() { if (this.updateHints) { this.updateHints(); this.setState({ newHints: true, }); } } // eslint-disable-next-line no-unused-vars updateHintsByDatas(datas) {} init() { this.store = { list: {} }; this.downloadStore = {}; } renderMore = (field, record) => { if (isEmpty(this.enabledItemActions)) { return null; } const content = this.renderMoreMenu(record); if (content === null) { return null; } return (