skyline-console/src/containers/List/index.jsx

1111 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 (
<Dropdown content={content} trigger="click" placement="bottomRight">
<Button icon="more" type="flat" />
</Dropdown>
);
};
renderMoreMenu = (record) => {
const items = this.enabledItemActions.map((action) => {
const show = isFunction(action.show)
? action.show(record)
: action.show || true;
if (!show) return null;
return (
<Menu.MenuItem key={action.key}>
<Icon name={action.icon} /> <span>{action.text}</span>
</Menu.MenuItem>
);
});
if (items.every((item) => item === null)) {
return null;
}
return <Menu onClick={this.handleMoreMenuClick(record)}>{items}</Menu>;
};
renderTable() {
try {
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 } = this.state;
return (
<BaseTable
resourceName={this.name}
data={this.getDataSource()}
// data={data}
columns={this.getColumns()}
filters={this.getFilters()}
timerFilter={timerFilter}
searchFilters={this.getSearchFilters()}
keyword={keyword}
pagination={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={this.tableHeight}
sortKey={sortKey}
sortOrder={sortOrder}
defaultSortKey={this.defaultSortKey}
defaultSortOrder={this.defaultSortOrder}
getDownloadData={this.getDownloadData}
containerProps={this.props}
expandable={this.expandable}
showTimeFilter={!!this.filterTimeKey}
filterTimeDefalutValue={this.filterTimeDefalutValue}
isPageByBack={this.isFilterByBackend}
isSortByBack={this.isSortByBackend}
isCourier={this.isCourier}
autoRefresh={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}
{...this.getEnabledTableProps()}
/>
);
} catch (e) {
// eslint-disable-next-line no-console
console.log(e);
const link = this.getUrl('/base/overview');
return <NotFound title={this.name} link={link} codeError />;
}
}
renderInfoHint() {
if (!this.infoMessage) {
return null;
}
return (
<Alert
message={this.infoMessage}
type="info"
showIcon
className={styles.hint}
/>
);
}
renderSuccessHint() {
if (!this.successMessage) {
return null;
}
return (
<Alert
message={this.successMessage}
type="success"
showIcon
closable
className={styles.hint}
onClose={this.onCloseSuccessHint}
/>
);
}
renderWarnHint() {
if (!this.warnMessage) {
return null;
}
return (
<Alert
message={this.warnMessage}
type="warning"
showIcon
className={styles.hint}
/>
);
}
renderErrorHint() {
if (!this.errorMessage) {
return null;
}
return (
<Alert
message={this.errorMessage}
type="error"
showIcon
closable
className={styles.hint}
/>
);
}
renderHint() {
const { newHints } = this.state;
if (
!newHints &&
!this.infoMessage &&
!this.warnMessage &&
!this.successMessage &&
!this.errorMessage
) {
return null;
}
return (
<div className={classnames(styles.hints, 'list-hints')}>
{this.renderInfoHint()}
{this.renderSuccessHint()}
{this.renderWarnHint()}
{this.renderErrorHint()}
</div>
);
}
renderHeader() {
return null;
}
render() {
if (this.endpointError) {
const link = this.getUrl('/base/overview');
return <NotFound title={this.name} link={link} endpointError />;
}
const table = this.renderTable();
return (
<div
className={classnames(styles.wrapper, 'list-container', this.className)}
>
{this.renderHeader()}
{this.renderHint()}
{table}
</div>
);
}
}