custom tabbar of console tab

This commit is contained in:
liugq 2021-11-01 17:30:41 +08:00
parent c7af0059f1
commit b8e6c8a579
14 changed files with 656 additions and 117 deletions

View File

@ -0,0 +1 @@
<svg width="2500" height="2500" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet"><path d="M255.96 134.393c0-21.521-13.373-40.117-33.223-47.43a75.239 75.239 0 0 0 1.253-13.791c0-39.909-32.386-72.295-72.295-72.295-23.193 0-44.923 11.074-58.505 30.088-6.686-5.224-14.835-7.94-23.402-7.94-21.104 0-38.446 17.133-38.446 38.446 0 4.597.836 9.194 2.298 13.373C13.582 81.739 0 100.962 0 122.274c0 21.522 13.373 40.327 33.431 47.64-.835 4.388-1.253 8.985-1.253 13.79 0 39.7 32.386 72.087 72.086 72.087 23.402 0 44.924-11.283 58.505-30.088 6.686 5.223 15.044 8.149 23.611 8.149 21.104 0 38.446-17.134 38.446-38.446 0-4.597-.836-9.194-2.298-13.373 19.64-7.104 33.431-26.327 33.431-47.64z" fill="#FFF"/><path d="M100.085 110.364l57.043 26.119 57.669-50.565a64.312 64.312 0 0 0 1.253-12.746c0-35.52-28.834-64.355-64.355-64.355-21.313 0-41.162 10.447-53.072 27.998l-9.612 49.73 11.074 23.82z" fill="#F4BD19"/><path d="M40.953 170.75c-.835 4.179-1.253 8.567-1.253 12.955 0 35.52 29.043 64.564 64.564 64.564 21.522 0 41.372-10.656 53.49-28.208l9.403-49.729-12.746-24.238-57.251-26.118-56.207 50.774z" fill="#3CBEB1"/><path d="M40.536 71.918l39.073 9.194 8.775-44.506c-5.432-4.179-11.91-6.268-18.805-6.268-16.925 0-30.924 13.79-30.924 30.924 0 3.552.627 7.313 1.88 10.656z" fill="#E9478C"/><path d="M37.192 81.32c-17.551 5.642-29.67 22.567-29.67 40.954 0 17.97 11.074 34.059 27.79 40.327l54.953-49.73-10.03-21.52-43.043-10.03z" fill="#2C458F"/><path d="M167.784 219.852c5.432 4.18 11.91 6.478 18.596 6.478 16.925 0 30.924-13.79 30.924-30.924 0-3.761-.627-7.314-1.88-10.657l-39.073-9.193-8.567 44.296z" fill="#95C63D"/><path d="M175.724 165.317l43.043 10.03c17.551-5.85 29.67-22.566 29.67-40.954 0-17.97-11.074-33.849-27.79-40.326l-56.415 49.311 11.492 21.94z" fill="#176655"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -7,11 +7,16 @@ import {DropdownItem} from './DropdownItem';
import {HealthStatusCircle} from '@/components/infini/health_status_circle'
class DropdownSelect extends React.Component{
state={
value: this.props.defaultValue,
loading: false,
hasMore: true,
overlayVisible: false,
constructor(props){
super(props)
this.state={
value: props.defaultValue,
loading: false,
hasMore: props.data.length > props.size,
overlayVisible: false,
data: (props.data || []).slice(0, props.size),
dataSource: [...props.data],
}
}
handleItemClick = (item)=>{
@ -28,47 +33,35 @@ class DropdownSelect extends React.Component{
}
componentDidMount(){
let me = this;
this.fetchData().then((data)=>{
let hasMore = true;
if(data.length < this.props.size){
hasMore = false;
}
me.setState({
hasMore
})
})
}
fetchData = (name)=>{
let me = this;
const {fetchData, size} = this.props;
let data = this.props.data || [];
return fetchData(name || '', size);
}
handleInfiniteOnLoad = (name) => {
let { data } = this.props;
handleInfiniteOnLoad = (current) => {
let {size } = this.props;
let targetLength = current * size;
let {hasMore, dataSource} = this.state;
if(dataSource.length < targetLength){
targetLength = dataSource.length;
hasMore = false
}
const newData = this.state.dataSource.slice(0, targetLength);
this.setState({
loading: true,
data: newData,
hasMore: hasMore,
})
this.fetchData(name).then((newdata)=>{
let newState = {
loading: false,
};
if(newdata.length < this.props.size){
//message.info("no more data");
newState.hasMore = false;
}
this.setState(newState);
});
}
handleInputChange = (e) =>{
const name = e.target.value;
const newData = this.props.data.filter(item=>{
return item.name.includes(name);
});
this.setState({
displayValue: name,
dataSource: newData,
data: newData,
hasMore: newData.length > this.props.size,
})
this.handleInfiniteOnLoad(name);
}
@ -79,7 +72,7 @@ class DropdownSelect extends React.Component{
let displayVaue = value[labelField];
const menu = (<div className={styles.dropmenu} style={{width: this.props.width}}>
<div className={styles.infiniteContainer} style={{height: this.props.height}}>
<div className="filter" style={{paddingTop: 10, paddingBottom:0}}>
<div className={styles.filter} style={{paddingTop: 10, paddingBottom:0}}>
<input className={styles['btn-ds']} style={{outline:'none'}} onChange={this.handleInputChange} placeholder="输入集群名称查找" value={this.state.displayValue||''} />
</div>
<InfiniteScroll
@ -89,8 +82,8 @@ class DropdownSelect extends React.Component{
useWindow={false}
>
<div className={styles.dslist}>
{(!this.props.data || !this.props.data.length)&& <div style={{display:'flex', justifyContent:'center', alignItems: 'center', height:50}}>匹配不到集群(匹配规则为前缀匹配)</div>}
{(this.props.data || []).map((item)=>{
{(!this.state.data || !this.state.data.length)&& <div style={{display:'flex', justifyContent:'center', alignItems: 'center', height:50}}>匹配不到集群(匹配规则为前缀匹配)</div>}
{(this.state.data || []).map((item)=>{
// return <div className={styles.item}>
// <Button key={item[labelField]}
// onClick={() => {

View File

@ -40,6 +40,14 @@
overflow: auto;
overflow-x: hidden;
max-height: 300px;
position: relative;
.filter{
position: sticky;
background-color: #fff;
top:0;
left:0;
width: 100%;
}
}
.loadingContainer {
position: absolute;

View File

@ -17,7 +17,7 @@ export default class GlobalHeader extends PureComponent {
}
componentDidMount() {
this.props.onFetchClusterList('', 200)
}
componentWillUnmount() {
@ -40,6 +40,9 @@ export default class GlobalHeader extends PureComponent {
};
render() {
const { collapsed, isMobile, logo, clusterVisible, clusterList, selectedCluster } = this.props;
if(clusterList.length == 0){
return null
}
return (
<div className={styles.header}>
{isMobile && (
@ -85,24 +88,6 @@ export default class GlobalHeader extends PureComponent {
}}
size={56}
fetchData={
this.props.onFetchClusterList
// (from, size)=>{
// return new Promise(resolve => {
// setTimeout(() => {
// let start = from;
// let data =[]
// for(let i = start + 1; i<start+size+1; i++){
// if(start+size > 56){
// break;
// }
// data.push('cluster'+i)
// }
// resolve(data)
// }, 2000)
// });
// }
}
data={clusterList}/>
<RightContent {...this.props} />
</div>

View File

@ -0,0 +1,127 @@
import Tabs from './index';
import { DndProvider, DragSource, DropTarget } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
const { TabPane } = Tabs;
// Drag & Drop node
class TabNode extends React.Component {
render() {
const { connectDragSource, connectDropTarget, children } = this.props;
return connectDragSource(connectDropTarget(children));
}
}
const cardTarget = {
drop(props, monitor) {
const dragKey = monitor.getItem().index;
const hoverKey = props.index;
if (dragKey === hoverKey) {
return;
}
props.moveTabNode(dragKey, hoverKey);
monitor.getItem().index = hoverKey;
},
};
const cardSource = {
beginDrag(props) {
return {
id: props.id,
index: props.index,
};
},
};
const WrapTabNode = DropTarget('DND_NODE', cardTarget, connect => ({
connectDropTarget: connect.dropTarget(),
}))(
DragSource('DND_NODE', cardSource, (connect, monitor) => ({
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging(),
}))(TabNode),
);
export class DraggableTabs extends React.Component {
constructor(props){
super(props);
this.state = {
order: props.initialOrder || [],
}
}
moveTabNode = (dragKey, hoverKey) => {
const newOrder = this.state.order.slice();
const { children } = this.props;
React.Children.forEach(children, c => {
if (newOrder.indexOf(c.key) === -1) {
newOrder.push(c.key);
}
});
const dragIndex = newOrder.indexOf(dragKey);
const hoverIndex = newOrder.indexOf(hoverKey);
newOrder.splice(dragIndex, 1);
newOrder.splice(hoverIndex, 0, dragKey);
this.setState({
order: newOrder,
});
if(typeof this.props.onTabNodeMoved == 'function'){
this.props.onTabNodeMoved(newOrder)
}
};
renderTabBar = (props, DefaultTabBar) => (
<DefaultTabBar {...props}>
{node => (
<WrapTabNode key={node.key} index={node.key} moveTabNode={this.moveTabNode}>
{node}
</WrapTabNode>
)}
</DefaultTabBar>
);
render() {
const { order } = this.state;
const { children } = this.props;
const tabs = [];
React.Children.forEach(children, c => {
tabs.push(c);
});
const orderTabs = tabs.slice().sort((a, b) => {
const orderA = order.indexOf(a.key);
const orderB = order.indexOf(b.key);
if (orderA !== -1 && orderB !== -1) {
return orderA - orderB;
}
if (orderA !== -1) {
return -1;
}
if (orderB !== -1) {
return 1;
}
const ia = tabs.indexOf(a);
const ib = tabs.indexOf(b);
return ia - ib;
});
return (
<DndProvider backend={HTML5Backend}>
<Tabs renderTabBar={this.renderTabBar} {...this.props}>
{orderTabs}
</Tabs>
</DndProvider>
);
}
}

View File

@ -0,0 +1,72 @@
import * as React from 'react';
import ScrollableInkTabBar from 'rc-tabs/lib/ScrollableInkTabBar';
import classNames from 'classnames';
import { TabsProps } from './index';
import {Icon} from 'antd';
export default class TabBar extends React.Component<TabsProps> {
static defaultProps = {
animated: true,
type: 'line',
};
render() {
const {
tabBarStyle,
animated,
renderTabBar,
tabBarExtraContent,
tabPosition,
prefixCls,
className,
size,
type,
} = this.props;
const inkBarAnimated = typeof animated === 'object' ? animated.inkBar : animated;
const isVertical = tabPosition === 'left' || tabPosition === 'right';
const prevIconType = isVertical ? 'up' : 'left';
const nextIconType = isVertical ? 'down' : 'right';
const prevIcon = (
<span className={`${prefixCls}-tab-prev-icon`}>
<Icon type={prevIconType} className={`${prefixCls}-tab-prev-icon-target`} />
</span>
);
const nextIcon = (
<span className={`${prefixCls}-tab-next-icon`}>
<Icon type={nextIconType} className={`${prefixCls}-tab-next-icon-target`} />
</span>
);
// Additional className for style usage
const cls: string = classNames(
`${prefixCls}-${tabPosition}-bar`,
{
[`${prefixCls}-${size}-bar`]: !!size,
[`${prefixCls}-card-bar`]: type && type.indexOf('card') >= 0,
},
className,
);
const renderProps = {
...this.props,
children: null,
inkBarAnimated,
extraContent: tabBarExtraContent,
style: tabBarStyle,
prevIcon,
nextIcon,
className: cls,
};
let RenderTabBar: React.ReactElement<any>;
if (renderTabBar) {
RenderTabBar = renderTabBar(renderProps, ScrollableInkTabBar);
} else {
RenderTabBar = <ScrollableInkTabBar {...renderProps} />;
}
return React.cloneElement(RenderTabBar);
}
}

View File

@ -0,0 +1,193 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import RcTabs, { TabPane } from 'rc-tabs';
import TabContent from 'rc-tabs/lib/TabContent';
import classNames from 'classnames';
import omit from 'omit.js';
import TabBar from './TabBar';
import {Icon} from 'antd';
import { ConfigConsumer, ConfigConsumerProps } from 'antd/lib/config-provider';
import './style/flex-bar.scss';
export type TabsType = 'line' | 'card' | 'editable-card';
export type TabsPosition = 'top' | 'right' | 'bottom' | 'left';
export interface TabsProps {
activeKey?: string;
defaultActiveKey?: string;
hideAdd?: boolean;
onChange?: (activeKey: string) => void;
onTabClick?: Function;
onPrevClick?: React.MouseEventHandler<HTMLElement>;
onNextClick?: React.MouseEventHandler<HTMLElement>;
tabBarExtraContent?: React.ReactNode | {left?: React.ReactNode, right?: React.ReactNode, append?: React.ReactNode} | null;
tabBarStyle?: React.CSSProperties;
type?: TabsType;
tabPosition?: TabsPosition;
onEdit?: (targetKey: string | React.MouseEvent<HTMLElement>, action: 'add' | 'remove') => void;
size?: 'large' | 'default' | 'small';
style?: React.CSSProperties;
prefixCls?: string;
className?: string;
animated?: boolean | { inkBar: boolean; tabPane: boolean };
tabBarGutter?: number;
renderTabBar?: (
props: TabsProps,
DefaultTabBar: React.ComponentClass<any>,
) => React.ReactElement<any>;
destroyInactiveTabPane?: boolean;
}
// Tabs
export interface TabPaneProps {
/** 选项卡头显示文字 */
tab?: React.ReactNode | string;
style?: React.CSSProperties;
closable?: boolean;
className?: string;
disabled?: boolean;
forceRender?: boolean;
key?: string;
}
export default class Tabs extends React.Component<TabsProps, any> {
static TabPane = TabPane as React.ClassicComponentClass<TabPaneProps>;
static defaultProps = {
hideAdd: false,
tabPosition: 'top' as TabsPosition,
};
componentDidMount() {
// const NO_FLEX = ' no-flex';
// const tabNode = ReactDOM.findDOMNode(this) as Element;
// if (tabNode && !isFlexSupported && tabNode.className.indexOf(NO_FLEX) === -1) {
// tabNode.className += NO_FLEX;
// }
}
removeTab = (targetKey: string, e: React.MouseEvent<HTMLElement>) => {
e.stopPropagation();
if (!targetKey) {
return;
}
const { onEdit } = this.props;
if (onEdit) {
onEdit(targetKey, 'remove');
}
};
handleChange = (activeKey: string) => {
const { onChange } = this.props;
if (onChange) {
onChange(activeKey);
}
};
createNewTab = (targetKey: React.MouseEvent<HTMLElement>) => {
const { onEdit } = this.props;
if (onEdit) {
onEdit(targetKey, 'add');
}
};
renderTabs = ({ getPrefixCls }: ConfigConsumerProps) => {
const {
prefixCls: customizePrefixCls,
className = '',
size,
type = 'line',
tabPosition,
children,
animated = true,
hideAdd,
} = this.props;
let { tabBarExtraContent } = this.props;
let tabPaneAnimated = typeof animated === 'object' ? animated.tabPane : animated;
// card tabs should not have animation
if (type !== 'line') {
tabPaneAnimated = 'animated' in this.props ? tabPaneAnimated : false;
}
const prefixCls = getPrefixCls('tabs', customizePrefixCls);
const cls = classNames(className, {
[`${prefixCls}-vertical`]: tabPosition === 'left' || tabPosition === 'right',
[`${prefixCls}-${size}`]: !!size,
[`${prefixCls}-card`]: type.indexOf('card') >= 0,
[`${prefixCls}-${type}`]: true,
[`${prefixCls}-no-animation`]: !tabPaneAnimated,
});
// only card type tabs can be added and closed
let childrenWithClose: React.ReactElement<any>[] = [];
if (type === 'editable-card') {
childrenWithClose = [];
React.Children.forEach(children as React.ReactNode, (child, index) => {
if (!React.isValidElement(child)) return child;
let { closable } = child.props;
closable = typeof closable === 'undefined' ? true : closable;
const closeIcon = closable ? (
<Icon
type="close"
className={`${prefixCls}-close-x`}
onClick={e => this.removeTab(child.key as string, e)}
/>
) : null;
childrenWithClose.push(
React.cloneElement(child, {
tab: (
<div className={closable ? undefined : `${prefixCls}-tab-unclosable`}>
{child.props.tab}
{closeIcon}
</div>
),
key: child.key || index,
}),
);
});
// Add new tab handler
// if (!hideAdd) {
// tabBarExtraContent = (
// <span>
// <Icon type="plus" className={`${prefixCls}-new-tab`} onClick={this.createNewTab} />
// {tabBarExtraContent}
// </span>
// );
// }
}
const newTabBarExtraContent: React.ReactNode = tabBarExtraContent ? (
<><div className={`${prefixCls}-extra-content ant-tabs-extra-append`}>{tabBarExtraContent?.append}</div><div className={`${prefixCls}-extra-content ant-tabs-extra-left`} style={{float:'left', margin:'0 10px'}}>{tabBarExtraContent.left}</div><div style={{float:'right', marginRight:10}} className={`${prefixCls}-extra-content ant-tabs-extra-right`}>{tabBarExtraContent.right}</div></>
) : null;
const { ...tabBarProps } = this.props;
const contentCls: string = classNames(
`${prefixCls}-${tabPosition}-content`,
type.indexOf('card') >= 0 && `${prefixCls}-card-content`,
);
return (
<RcTabs
{...this.props}
prefixCls={prefixCls}
className={cls}
tabBarPosition={tabPosition}
renderTabBar={() => (
<TabBar {...omit(tabBarProps, ['className'])} tabBarExtraContent={newTabBarExtraContent} />
)}
renderTabContent={() => (
<TabContent className={contentCls} animated={tabPaneAnimated} animatedWithMargin />
)}
onChange={this.handleChange}
>
{childrenWithClose.length > 0 ? childrenWithClose : children}
</RcTabs>
);
};
render() {
return <div className="flex-tabbar"><ConfigConsumer>{this.renderTabs}</ConfigConsumer></div>;
}
}

View File

@ -0,0 +1,24 @@
.ant-tabs-top-bar{
display: flex;
}
.ant-tabs-extra-left{
order: 1;
flex: 0 0 auto;
}
.ant-tabs-nav-container{
order: 2;
flex: 0 0 auto;
}
.flex-tabbar .ant-tabs-top-bar .ant-tabs-nav-container{
max-width: calc(100% - 140px);
}
.ant-tabs-extra-append{
order: 3;
margin-left: 10px;
}
.ant-tabs-extra-right{
order: 4;
margin-left: auto;
flex: 0 0 auto;
}

View File

@ -32,4 +32,12 @@
background-color: #cce1f0;
}
}
.ant-tabs-right-content{
padding-right: 0px;
}
.ant-tabs-right-bar .ant-tabs-tab{
padding: 8px 10px;
writing-mode: vertical-lr;
height: max-content;
}
}

View File

@ -12,9 +12,10 @@ import {EditorContextProvider} from '../contexts/editor_context/editor_context';
import { ServicesContextProvider } from '../contexts/services_context';
import { createHistory, History, createStorage, createSettings } from '../services';
import { create } from '../storage/local_storage_object_client';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem,EuiCodeBlock } from '@elastic/eui';
import {RequestStatusBar} from './request_status_bar';
import useEventListener from '@/lib/hooks/use_event_listener';
import {Tabs} from 'antd';
interface props {
selectedCluster: any;
@ -78,7 +79,28 @@ const ConsoleWrapper = ({
</div>
</Panel>
<Panel style={{ height: '100%', position: 'relative', minWidth: PANEL_MIN_WIDTH, paddingBottom:30 }} initialWidth={INITIAL_PANEL_WIDTH}>
<ConsoleOutput clusterID={selectedCluster.id} />
<Tabs tabPosition='right' style={{height:'100%', width:'100%'}} size="small">
<Tabs.TabPane tab="Result" key="result">
<div style={{height:height-30}}>
<ConsoleOutput clusterID={selectedCluster.id} />
</div>
</Tabs.TabPane>
<Tabs.TabPane tab="Headers" key="headers">
<Tabs>
<Tabs.TabPane tab="Request" key="1">
<EuiCodeBlock language="text" isCopyable paddingSize="s">
{lastDatum?.request.header}
</EuiCodeBlock>
</Tabs.TabPane>
<Tabs.TabPane tab="Response" key="2">
<EuiCodeBlock language="text" isCopyable paddingSize="s">
{lastDatum?.response.header}
</EuiCodeBlock>
</Tabs.TabPane>
</Tabs>
</Tabs.TabPane>
</Tabs>
<div style={{background:'#fff', position:'absolute', right:0, bottom:0, width: '100%', height:30, zIndex:1001, borderTop: '1px solid #eee'}}>
<RequestStatusBar
requestInProgress={requestInProgress}

View File

@ -89,7 +89,6 @@ export const RequestStatusBar = ({
<EuiBadge color="default">{selectedCluster.version}</EuiBadge>
</div>
</div>);
const [headerInfoVisible, setHeaderInfoVisible] = React.useState(false)
if (requestInProgress) {
content = (
@ -140,12 +139,6 @@ const [headerInfoVisible, setHeaderInfoVisible] = React.useState(false)
</EuiText>
</EuiToolTip>
</div>
<div className="info-item">
{/* <Button type="link" onClick={()=>{setHeaderInfoVisible(true)}}> */}
{/* <FormattedMessage id="console.request.headers"/> */}
Headers
{/* </Button> */}
</div>
</div>
</>
);
@ -154,32 +147,7 @@ const [headerInfoVisible, setHeaderInfoVisible] = React.useState(false)
return (
<div className="request-status-bar">
{left? <div className="bar-item">{clusterContent}</div>:
[<div className="bar-item">{content}</div>,
<Drawer title="Request header info"
style={{zIndex:1004}}
width={520}
mask={false}
// getContainer={container.current}
destroyOnClose={true}
visible={headerInfoVisible}
onClose={()=>{setHeaderInfoVisible(false)}}
>
<Tabs>
<Tabs.TabPane tab="Request" key="1">
<div>
<EuiCodeBlock language="text" isCopyable paddingSize="s">
{requestResult?.requestHeader}
</EuiCodeBlock>
</div>
</Tabs.TabPane>
<Tabs.TabPane tab="Response" key="2">
<EuiCodeBlock language="text" isCopyable paddingSize="s">
{requestResult?.responseHeader}
</EuiCodeBlock>
</Tabs.TabPane>
</Tabs>
</Drawer>]
[<div className="bar-item">{content}</div>,]
}
</div>

View File

@ -1,7 +1,8 @@
import Console from '../../components/kibana/console/components/Console';
import {connect} from 'dva';
import {Button, Icon, Menu, Dropdown, Tabs} from 'antd';
// import Tabs from '@/components/infini/tabs';
import {Button, Icon, Menu, Dropdown, } from 'antd';
import Tabs from '@/components/infini/tabs';
import {DraggableTabs} from '@/components/infini/tabs/DraggableTabs';
import {useState, useReducer, useCallback, useEffect, useMemo, useRef, useLayoutEffect} from 'react';
import {useLocalStorage} from '@/lib/hooks/storage';
import {setClusterID} from '../../components/kibana/console/modules/mappings/mappings';
@ -9,6 +10,7 @@ import {TabTitle} from './console_tab_title';
import '@/assets/utility.scss';
import { Resizable } from "re-resizable";
import {ResizeBar} from '@/components/infini/resize_bar';
import NewTabMenu from './NewTabMenu';
const { TabPane } = Tabs;
@ -94,6 +96,11 @@ const consoleTabReducer = (state: any, action: any) => {
panes,
});
break;
case 'saveOrder':
newState = ({
...state,
order: action.payload.order,
});
default:
}
// setLocalState(newState);
@ -123,8 +130,8 @@ export const ConsoleUI = ({selectedCluster,
return cm;
}
(clusterList || []).map((cluster: any)=>{
cluster.status = clusterStatus[cluster.id].health?.status;
if(!clusterStatus[cluster.id].available){
cluster.status = clusterStatus[cluster.id]?.health?.status;
if(!clusterStatus[cluster.id]?.available){
cluster.status = 'unavailable';
}
cm[cluster.id] = cluster;
@ -184,7 +191,7 @@ export const ConsoleUI = ({selectedCluster,
};
const newTabClick = useCallback((param: any)=>{
const cluster = clusterList.find(item=>item.id == param.key);
const cluster = clusterList.find(item=>item.id == param.id);
if(!cluster){
console.log('cluster not found')
return;
@ -198,11 +205,16 @@ export const ConsoleUI = ({selectedCluster,
},[clusterList])
const menu = (
<Menu onClick={newTabClick}>
{(clusterList||[]).map((cluster:any)=>{
return <Menu.Item key={cluster.id}>{cluster.name}</Menu.Item>
})}
</Menu>
// <Menu onClick={newTabClick}>
// {(clusterList||[]).map((cluster:any)=>{
// return <Menu.Item key={cluster.id}>{cluster.name}</Menu.Item>
// })}
// </Menu>
<NewTabMenu data={clusterList}
onItemClick={newTabClick}
clusterStatus={clusterStatus}
size={10}
width="300px"/>
);
const rootRef = useRef(null);
@ -220,26 +232,35 @@ export const ConsoleUI = ({selectedCluster,
setIsFullscreen(!isFullscreen)
}
const tabBarExtra =(
const tabBarExtra ={
left: <div>
{minimize? <Button size="small" onClick={onMinimizeClick} style={{marginLeft:5}} title="Close">
{/* <Icon type="minus"/> */}
<Icon type="poweroff"/>
</Button>:null}
</div>,
right:(
<div>
<Dropdown overlay={menu}>
<Button size="small" style={{marginRight:5}}>
<Icon type="plus"/>
</Button>
</Dropdown>
{isFullscreen?
<Button size="small" onClick={fullscreenClick}>
<Icon type="fullscreen-exit"/>
</Button>:
<Button size="small" onClick={fullscreenClick}>
<Button size="small" onClick={fullscreenClick} title="Fullscreen">
<Icon type="fullscreen"/>
</Button>
}
{minimize? <Button size="small" onClick={onMinimizeClick} style={{marginLeft:5}}>
<Icon type="minus"/>
</Button>:null}
</div>
);
),
append:(
<div>
<Dropdown overlay={menu} placement="bottomLeft">
<Button size="small" style={{marginRight:5}}>
<Icon type="plus"/>
</Button>
</Dropdown>
</div>
)
};
setClusterID(tabState.activeKey?.split(':')[0]);
const panes = tabState.panes.filter((pane: any)=>{
@ -268,6 +289,14 @@ export const ConsoleUI = ({selectedCluster,
const enableWindowScroll = ()=>{
document.body.style.overflow = '';
}
const onTabNodeMoved=(newOrder:string[])=>{
dispatch({
type:'saveOrder',
payload: {
order: newOrder,
}
})
}
return (
@ -275,7 +304,7 @@ export const ConsoleUI = ({selectedCluster,
defaultSize={{
height: editorHeight||'50vh'
}}
minHeight={200}
minHeight={70}
maxHeight="100vh"
handleComponent={{ top: <ResizeBar/> }}
onResize={onResize}
@ -294,13 +323,15 @@ export const ConsoleUI = ({selectedCluster,
onMouseOut={enableWindowScroll}
id="console"
ref={rootRef} >
<Tabs
<DraggableTabs
onChange={onChange}
activeKey={tabState.activeKey}
type="editable-card"
onEdit={onEdit}
hideAdd
initialOrder={tabState.order}
tabBarExtraContent={tabBarExtra}
onTabNodeMoved={onTabNodeMoved}
>
{panes.map(pane => (
<TabPane tab={<TabTitle title={pane.title} onTitleChange={(title)=>{saveTitle(pane.key, title)}}/>} key={pane.key} closable={pane.closable}>
@ -308,7 +339,7 @@ export const ConsoleUI = ({selectedCluster,
{/* {pane.content} */}
</TabPane>
))}
</Tabs>
</DraggableTabs>
</div>
</Resizable>
);

View File

@ -0,0 +1,100 @@
import { Button, Dropdown, List, Spin, message, Icon, Input } from 'antd';
import * as React from 'react';
import InfiniteScroll from 'react-infinite-scroller';
import styles from '@/components/GlobalHeader/DropdownSelect.less';
import _ from "lodash";
import {DropdownItem} from '@/components/GlobalHeader/DropdownItem';
import {HealthStatusCircle} from '@/components/infini/health_status_circle'
class NewTabMenu extends React.Component{
handleItemClick = (item)=>{
const onItemClick = this.props.onItemClick;
if(onItemClick && typeof onItemClick == 'function'){
onItemClick(item)
}
}
constructor(props){
super(props);
this.state={
loading: false,
hasMore: (props.data || []).length > props.size,
data: (props.data || []).slice(0, props.size || 10),
initialLoad: true,
dataSource: [...props.data],
dataSourceKey: 1,
}
}
componentDidMount(){
}
handleInfiniteOnLoad = (current) => {
let {size } = this.props;
let targetLength = current * size;
let {hasMore, dataSource} = this.state;
if(dataSource.length < targetLength){
targetLength = dataSource.length;
hasMore = false
}
const newData = this.state.dataSource.slice(0, targetLength);
this.setState({
data: newData,
hasMore: hasMore,
})
}
handleInputChange = (e) =>{
const name = e.target.value;
const newData = this.props.data.filter(item=>{
return item.name.includes(name);
});
this.setState({
displayValue: name,
dataSource: newData,
data: newData,
hasMore: newData.length > this.props.size,
})
}
render(){
const {clusterStatus} = this.props;
return (<div className={styles.dropmenu} style={{width: this.props.width}}>
<div className={styles.infiniteContainer} style={{height: this.props.height}}>
<div className={styles.filter} style={{paddingTop: 10, paddingBottom:0}}>
<input className={styles['btn-ds']} style={{outline:'none'}} onChange={this.handleInputChange} placeholder="输入集群名称查找" value={this.state.displayValue||''} />
</div>
<InfiniteScroll
initialLoad={this.state.initialLoad}
loadMore={this.handleInfiniteOnLoad}
hasMore={!this.state.loading && this.state.hasMore}
useWindow={false}
>
<div className={styles.dslist}>
{(!this.state.data || !this.state.data.length)&& <div style={{display:'flex', justifyContent:'center', alignItems: 'center', height:50}}>匹配不到集群(匹配规则为前缀匹配)</div>}
{(this.state.data || []).map((item)=>{
const cstatus = clusterStatus ? clusterStatus[item.id] : null;
return <DropdownItem key={item.id}
clusterItem={item}
clusterStatus={cstatus}
onClick={() => {
this.handleItemClick(item)
}}
/>
})}
</div>
</InfiniteScroll>
</div>
{!this.state.loading && this.state.hasMore && (
<div style={{textAlign:'center', marginTop: 10, color:'#ccc'}}>
pull load more
</div>
)}
</div>);
}
}
export default NewTabMenu;

View File

@ -1,5 +1,11 @@
import {useState, useRef, useEffect} from 'react';
import './console_tab_title.scss';
import ElasticSvg from '@/assets/elasticsearch.svg';
import {Icon} from 'antd';
const ElasticIcon = () => (
<img height="14px" width="14px" src={ElasticSvg} />
);
interface TabTitleProps {
title: string,
@ -28,7 +34,8 @@ export const TabTitle = ({title, onTitleChange}: TabTitleProps)=>{
onBlur={()=>{
setEditable(false)
}}
onChange={onValueChange}/>:value}
onChange={onValueChange}/>:
<div style={{display:'flex', alignItems:'center'}}><Icon component={ElasticIcon} />{value}</div>}
</div>)
}