// 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,
debounce,
} 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/skyline/policy';
import NotFound from 'components/Cards/NotFound';
import { getTags } from 'components/MagicInput';
import { getPath, getLinkRender } from 'utils/route-map';
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,
tableHeight: this.getTableHeight(),
};
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.setTableHeight = this.setTableHeight.bind(this);
this.debounceSetTableHeight = this.debounceSetTableHeight.call(this);
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 = this.initFilter;
const { tags = [] } = getTags(params, this.searchFilters);
if (!tags.length && !this.filterTimeKey) {
const { limit, page } = this.store.list;
this.list.filters = {};
this.handleFetch({ ...params, limit, page }, true);
}
}
});
window.addEventListener('resize', this.debounceSetTableHeight);
}
componentDidUpdate(prevProps) {
if (this.inDetailPage) {
const { detail: oldDetail } = prevProps;
const { detail: newDetail } = this.props;
if (
!isEmpty(oldDetail) &&
!isEmpty(newDetail) &&
!isEqual(oldDetail, newDetail)
) {
this.handleRefresh(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');
}
window.removeEventListener('resize', this.debounceSetTableHeight);
}
get policy() {
return '';
}
get name() {
return '';
}
get title() {
return `${this.name}s`;
}
get className() {
return '';
}
get inDetailPage() {
const { detail } = this.props;
return !!detail;
}
get detailName() {
if (!this.inDetailPage) {
return '';
}
const { detailName } = this.props;
return detailName;
}
get shouldRefreshDetail() {
return true;
}
get location() {
return this.props.location;
}
get isAdminPage() {
const { pathname } = this.location;
return isAdminPage(pathname);
}
get hasAdminRole() {
return this.props.rootStore.hasAdminRole;
}
getRouteName(routeName) {
return this.isAdminPage ? `${routeName}Admin` : routeName;
}
getRoutePath(routeName, params = {}, query = {}) {
const realName = this.getRouteName(routeName);
return getPath({ key: realName, params, query });
}
getLinkRender(routeName, value, params = {}, query = {}) {
const realName = this.getRouteName(routeName);
return getLinkRender({ key: realName, params, query, value });
}
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 initFilter() {
const params = parse(this.location.search.slice(1));
return params || {};
}
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;
}
getTableHeight() {
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';
}
get forceRefreshTopDetailWhenListRefresh() {
return false;
}
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,
};
}
getBaseTableProps() {
const {
keyword,
selectedRowKeys,
total,
page,
limit,
silent,
sortKey,
sortOrder,
timerFilter,
} = this.list;
const pagination = {
total,
current: Number(page),
pageSize: limit || 10,
// eslint-disable-next-line no-shadow
showTotal: (total) => t('Total {total} items', { total }),
showSizeChanger: true,
};
if (this.pageSizeOptions) {
pagination.pageSizeOptions = this.pageSizeOptions;
}
const { autoRefresh, tableHeight } = this.state;
return {
resourceName: this.name,
detailName: this.detailName,
data: this.getDataSource(),
// data:data,
columns: this.getColumns(),
filters: this.getFilters(),
timerFilter,
searchFilters: this.getSearchFilters(),
keyword,
pagination,
primaryActions: this.primaryActions,
batchActions: this.batchActions,
itemActions: this.itemActions,
getCheckboxProps: this.getCheckboxProps,
isLoading: this.isLoading,
silentLoading: silent,
rowKey: this.rowKey,
selectedRowKeys: toJS(selectedRowKeys),
scrollY: tableHeight,
sortKey,
sortOrder,
defaultSortKey: this.defaultSortKey,
defaultSortOrder: this.defaultSortOrder,
getDownloadData: this.getDownloadData,
containerProps: this.props,
expandable: this.expandable,
showTimeFilter: !!this.filterTimeKey,
filterTimeDefaultValue: this.filterTimeDefaultValue,
isPageByBack: this.isFilterByBackend,
isSortByBack: this.isSortByBackend,
isCourier: this.isCourier,
autoRefresh,
startRefreshAuto: this.startRefreshAuto,
stopRefreshAuto: this.onStopRefreshAuto,
onClickAction: this.onClickAction,
onFinishAction: this.onFinishAction,
onCancelAction: this.onCancelAction,
dataDurationAuto: this.dataDurationAuto,
handleInputFocus: this.handleInputFocus,
hideTotal: this.hideTotal,
hideDownload: this.hideDownload,
primaryActionsExtra: this.primaryActionsExtra,
isAdminPage: this.isAdminPage,
initFilter: this.initFilter,
...this.getEnabledTableProps(),
};
}
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);
}
setTableHeight() {
const currentTableHeight = this.getTableHeight();
const { tableHeight } = this.state;
if (currentTableHeight !== tableHeight) {
this.setState({
tableHeight: currentTableHeight,
});
}
}
getColumns() {
return [];
}
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 === 401) {
const title = t('The session has expired, please log in again.');
Notify.errorWithDetail(null, title);
} else if (status === 500) {
const systemErr = t('System is error, please try again later.');
const title = `${t('Get {name} error.', {
name: this.name.toLowerCase(),
})} ${systemErr}`;
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 download 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];
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.getSearchFilters().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 items = [];
if (this.isFilterByBackend) {
items = toJS(data);
} else {
items = (toJS(data) || []).filter((it) =>
this.filterData(it, toJS(newFilters), toJS(timeFilter))
);
this.updateList({ total: items.length });
}
const hasTransData = items.some((item) =>
this.itemInTransitionFunction(item)
);
if (hasTransData) {
this.setRefreshDataTimerTransition();
} else {
this.setRefreshDataTimerAuto();
}
this.updateHintsByData(items);
this.setTableHeight();
return items;
};
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.inDetailPage &&
(force || this.forceRefreshTopDetailWhenListRefresh) &&
this.shouldRefreshDetail
) {
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 = () => {};
debounceSetTableHeight() {
return debounce(this.setTableHeight, 1000);
}
// eslint-disable-next-line no-unused-vars
updateHintsByOthers() {
if (this.updateHints) {
this.updateHints();
setTimeout(this.setTableHeight, 0);
this.setState({
newHints: true,
});
}
}
// eslint-disable-next-line no-unused-vars
updateHintsByData(data) {}
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 (