modify global cluster selector and view style

This commit is contained in:
silenceqi 2021-09-01 20:25:42 +08:00
parent b8f1297a19
commit 5a7f978fea
21 changed files with 344 additions and 394 deletions

View File

@ -4,6 +4,7 @@ import (
"errors"
_ "expvar"
"infini.sh/framework"
"infini.sh/framework/core/elastic"
"infini.sh/framework/core/env"
"infini.sh/framework/core/module"
"infini.sh/framework/core/orm"
@ -70,6 +71,7 @@ func main() {
}, func() {
orm.RegisterSchemaWithIndexName(model.Dict{}, "dict")
orm.RegisterSchemaWithIndexName(model.Reindex{}, "reindex")
orm.RegisterSchemaWithIndexName(elastic.IndexPattern{}, "view")
})
}

View File

@ -14,6 +14,11 @@ const config = {
title: '500',
desc: '抱歉,服务器出错了',
},
empty: {
img: 'https://gw.alipayobjects.com/zos/rmsportal/KpnpchXsobRgLElEozzI.svg',
title: '找不到数据',
desc: '当前集群找到相关数据',
}
};
export default config;

View File

@ -0,0 +1,39 @@
import {Icon} from 'antd';
import styles from './DropdownSelect.less';
import {HealthStatusCircle, ClusterHealthStatus} from '@/components/infini/health_status_circle'
export interface ClusterItem {
id: string
name: string
version: string
endpoint: string
}
export interface ClusterStatus {
cluster_available: boolean
health_status: ClusterHealthStatus
nodes_count: number
}
interface props {
clusterItem?: ClusterItem
clusterStatus?: ClusterStatus
onClick: React.MouseEventHandler<HTMLDivElement> | undefined
isSelected: boolean
}
export const DropdownItem = ({
clusterItem,
clusterStatus,
onClick,
isSelected
}:props)=>{
return <div className={styles["dropdown-item"] +" " + (isSelected ? styles['selected']: '')} onClick={onClick}>
<div className={styles["wrapper"]}>
{clusterStatus?.cluster_available ? <HealthStatusCircle status={clusterStatus?.health_status} />: <Icon type='close-circle' style={{width:14, height:14, color:'red',borderRadius: 14, boxShadow: '0px 0px 5px #555'}} />}
<span className={styles["name"]} >{clusterItem?.name}</span>
<div className={styles["version"]}>{clusterItem?.version}</div>
</div>
</div>
}

View File

@ -1,20 +1,24 @@
import { Button, Dropdown, List, Spin, message, Icon } from 'antd';
import { Button, Dropdown, List, Spin, message, Icon, Input } from 'antd';
import React from 'react';
import InfiniteScroll from 'react-infinite-scroller';
import styles from './DropdownSelect.less';
import _ from "lodash";
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,
}
handleItemClick = (item)=>{
let preValue = this.props.value || this.state.value;
this.setState({
value: item,
overlayVisible: false,
},()=>{
let onChange = this.props.onChange;
if(preValue != item && onChange && typeof onChange == 'function'){
@ -35,75 +39,74 @@ class DropdownSelect extends React.Component{
})
})
}
fetchData = ()=>{
fetchData = (name)=>{
let me = this;
const {fetchData, size} = this.props;
let data = this.props.data || [];
let from = data.length;
return fetchData(from, size);
return fetchData(name || '', size);
}
handleInfiniteOnLoad = (page) => {
handleInfiniteOnLoad = (name) => {
let { data } = this.props;
this.setState({
loading: true,
})
this.fetchData().then((newdata)=>{
this.fetchData(name).then((newdata)=>{
let newState = {
loading: false,
};
if(newdata.length < this.props.size){
message.info("no more data");
//message.info("no more data");
newState.hasMore = false;
}
this.setState(newState);
});
}
handleInputChange = (e) =>{
const name = e.target.value;
this.setState({
displayValue: name,
})
this.handleInfiniteOnLoad(name);
}
render(){
let me = this;
const {labelField} = this.props;
const {labelField, clusterStatus} = this.props;
let value = this.props.value || this.state.value;
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}}>
<input className={styles['btn-ds']} style={{outline:'none'}} onChange={this.handleInputChange} placeholder="输入集群名称查找" value={this.state.displayValue||''} />
</div>
<InfiniteScroll
initialLoad={false}
loadMore={this.handleInfiniteOnLoad}
hasMore={!this.state.loading && this.state.hasMore}
useWindow={false}
>
{/* <List
grid={{
gutter: 16,
sm: 4,
xs: 3
}}
dataSource={this.props.data}
renderItem={item => {
return (
<List.Item key={item[labelField]}>
<Button onClick={() => {
this.handleItemClick(item)
}}
className={_.isEqual(item, value) ? styles.btnitem + " " + styles.selected : styles.btnitem}>{item[labelField]}</Button>
</List.Item>
)
}}
>
{this.state.loading && this.state.hasMore && (
<div className={styles.loadingContainer}>
<Spin />
</div>
)}
</List> */}
<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)=>{
return <div className={styles.item}><Button key={item[labelField]} onClick={() => {
this.handleItemClick(item)
}}
className={_.isEqual(item, value) ? styles.btnitem + " " + styles.selected : styles.btnitem}>{item[labelField]}</Button></div>
// return <div className={styles.item}>
// <Button key={item[labelField]}
// onClick={() => {
// this.handleItemClick(item)
// }}
// className={_.isEqual(item, value) ? styles.btnitem + " " + styles.selected : styles.btnitem}>{item[labelField]}</Button>
// </div>
const cstatus = clusterStatus ? clusterStatus[item.id] : null;
return <DropdownItem key={item.id}
isSelected={item.id===value.id}
clusterItem={item}
clusterStatus={cstatus}
onClick={() => {
this.handleItemClick(item)
}}
/>
})}
</div>
</InfiniteScroll>
@ -114,11 +117,23 @@ class DropdownSelect extends React.Component{
</div>
)}
</div>);
const cstatus = clusterStatus ? clusterStatus[value?.id] : null;
return(
this.props.visible ?
(<Dropdown overlay={menu} placement="bottomLeft">
<Button className={styles['btn-ds']}>{value[labelField]} <Icon style={{float: 'right', marginTop: 3}}
type="caret-down"/></Button>
(<Dropdown overlay={menu} placement="bottomLeft" visible={this.state.overlayVisible}
onVisibleChange={(flag)=>{
this.setState({ overlayVisible: flag });
}}>
{/* <Button className={styles['btn-ds']}>{value[labelField]} <Icon style={{float: 'right', marginTop: 3}}
type="caret-down"/></Button> */}
<span style={{position:'relative'}}>
<i style={{position: 'absolute', left:15,zIndex:10, top: -28}}>
{cstatus?.cluster_available ? <HealthStatusCircle status={cstatus?.health_status} />: <Icon type='close-circle' style={{width:14, height:14, color:'red',borderRadius: 14, boxShadow: '0px 0px 5px #555'}} />}
</i>
<input className={styles['btn-ds']} style={{outline:'none', paddingLeft:22}} value={value[labelField]} readOnly={true} />
<Icon style={{position:'absolute', top:-6, right:-4}} type="caret-down"/>
</span>
</Dropdown>) : ""
)
}

View File

@ -9,6 +9,7 @@
margin-left: 15px;
position: relative;
bottom: 8px;
height: 32px;
span{
color: #333;
}
@ -16,7 +17,7 @@
.dropmenu{
box-shadow: 0 0 15px 0 rgba(0, 0, 0, .15);
padding: 20px;
padding: 0px;
padding-bottom: 4px;
width: 500px;
background: #fff;
@ -56,4 +57,28 @@
.dslist{
display: inline-block;
width: 100%;
}
.dropdown-item{
box-shadow: 0 0 1px 0 rgba(196, 191, 191, 0.15);
.wrapper{
display: flex;
align-items: center;
padding: 10px;
padding-left: 15px;
.name{
margin-left: 5px;
}
.version{
margin-left: auto;
}
}
:hover{
background-color: #939ea0;
color: #fff;
cursor: pointer;
}
&.selected {
background-color: #eee;
}
}

View File

@ -1,5 +1,5 @@
import React, { PureComponent } from 'react';
import { Icon } from 'antd';
import { Icon, Select } from 'antd';
import Link from 'umi/link';
import Debounce from 'lodash-decorators/debounce';
import styles from './index.less';
@ -13,7 +13,7 @@ const path=require('path');
export default class GlobalHeader extends PureComponent {
constructor(props) {
super(props);
super(props);
}
componentDidMount() {
@ -53,6 +53,7 @@ export default class GlobalHeader extends PureComponent {
onClick={this.toggle}
/>
<DropdownSelect defaultValue={selectedCluster}
clusterStatus={this.props.clusterStatus}
value={selectedCluster}
labelField="name"
visible={clusterVisible}

View File

@ -0,0 +1,20 @@
export type ClusterHealthStatus = 'green' | 'yellow' | 'red';
const statusColorMap: Record<string, string> = {
'green': '#39b362',
'yellow': 'yellow',
'red': 'red',
}
export function convertStatusToColor(status: ClusterHealthStatus){
return statusColorMap[status];
}
interface props {
status: ClusterHealthStatus
}
export const HealthStatusCircle = ({status}: props)=>{
const color = convertStatusToColor(status);
return <div style={{background: color, height:14, width:14, borderRadius: 14, boxShadow: '0px 0px 5px #999', display: 'inline-block'}}></div>
}

View File

@ -626,10 +626,6 @@ export class IndexPatternsService {
});
}
/**
* Deletes an index pattern from .kibana index
* @param indexPatternId: Id of kibana Index Pattern to delete
*/
async delete(indexPatternId: string) {
indexPatternCache.clear(indexPatternId);
return this.savedObjectsClient.delete(savedObjectType, indexPatternId);

View File

@ -23,8 +23,8 @@ import moment from 'moment';
import { areRefreshIntervalsDifferent, areTimeRangesDifferent } from './lib/diff_time_picker_vals';
import { getForceNow } from './lib/get_force_now';
import { TimefilterConfig, InputTimeRange, TimeRangeBounds } from './types';
import { getTime, RefreshInterval, TimeRange } from '../../../common';
import { calculateBounds } from '../../../common/query/timefilter/get_time';
import { RefreshInterval, TimeRange } from '../../../common';
import {getTime, calculateBounds } from '../../../common/query/timefilter/get_time';
import { TimeHistoryContract } from './time_history';
import { IndexPattern } from '../../index_patterns';

View File

@ -218,7 +218,7 @@ export class CreateIndexPatternWizard extends Component<
return <LoadingState />;
}
const header = this.renderHeader();
// const header = this.renderHeader();
if (step === 1) {
const { location } = this.props;
@ -226,8 +226,8 @@ export class CreateIndexPatternWizard extends Component<
return (
<EuiPageContent>
{header}
<EuiHorizontalRule />
{/* {header}
<EuiHorizontalRule /> */}
<StepIndexPattern
allIndices={allIndices}
initialQuery={indexPattern || initialQuery}
@ -245,8 +245,8 @@ export class CreateIndexPatternWizard extends Component<
if (step === 2) {
return (
<EuiPageContent>
{header}
<EuiHorizontalRule />
{/* {header}
<EuiHorizontalRule /> */}
<StepTimeField
indexPattern={indexPattern}
viewName={viewName}

View File

@ -18,76 +18,32 @@
*/
import './empty_index_pattern_prompt.scss';
import React from 'react';
import { EuiPageContent, EuiSpacer, EuiText, EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
import { EuiDescriptionListTitle } from '@elastic/eui';
import { EuiDescriptionListDescription, EuiDescriptionList } from '@elastic/eui';
import { EuiLink } from '@elastic/eui';
import { getListBreadcrumbs } from '../../breadcrumbs';
import { IndexPatternCreationOption } from '../../types';
import { CreateButton } from '../../create_button';
import { Illustration } from './assets/index_pattern_illustration';
import { ManagementAppMountParams } from '../../../../../management/public';
import Link from 'umi/link';
import Exception from '@/components/Exception';
import {Button} from 'antd';
interface Props {
canSave: boolean;
creationOptions: IndexPatternCreationOption[];
docLinksIndexPatternIntro: string;
setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs'];
}
export const EmptyIndexPatternPrompt = ({
canSave,
creationOptions,
docLinksIndexPatternIntro,
setBreadcrumbs,
}: Props) => {
setBreadcrumbs(getListBreadcrumbs());
const actions = (<div>
<Button type="primary" onClick={()=>{
creationOptions[0].onClick();
}}></Button>
</div>)
return (
<EuiPageContent
data-test-subj="emptyIndexPatternPrompt"
className="inpEmptyIndexPatternPrompt"
grow={false}
horizontalPosition="center"
>
<EuiFlexGroup gutterSize="xl" alignItems="center" direction="rowReverse" wrap>
{/* <EuiFlexItem grow={1} className="inpEmptyIndexPatternPrompt__illustration">
<Illustration />
</EuiFlexItem> */}
<EuiFlexItem grow={2} className="inpEmptyIndexPatternPrompt__text">
<EuiText grow={false}>
<h2>
Elasticsearch
<br />
</h2>
<p>
server_log
server_log, server_log* server_log
</p>
{canSave && (
<CreateButton options={creationOptions}>
</CreateButton>
)}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xxl" />
<EuiDescriptionList className="inpEmptyIndexPatternPrompt__footer" type="responsiveColumn">
<EuiDescriptionListTitle className="inpEmptyIndexPatternPrompt__title">
?
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
<EuiLink href={docLinksIndexPatternIntro} target="_blank" external>
</EuiLink>
</EuiDescriptionListDescription>
</EuiDescriptionList>
</EuiPageContent>
<Exception
type="404"
title="没有视图"
linkElement={Link}
desc={'当前集群找不到任何数据视图'}
actions={actions}
/>
);
};

View File

@ -17,165 +17,28 @@
* under the License.
*/
import './empty_state.scss';
import React from 'react';
import { DocLinksStart, ApplicationStart } from 'kibana/public';
import {
EuiPageContentHeader,
EuiPageContentHeaderSection,
EuiTitle,
EuiPageContentBody,
EuiPageContent,
EuiIcon,
EuiSpacer,
EuiFlexItem,
EuiDescriptionList,
EuiFlexGrid,
EuiCard,
EuiLink,
EuiText,
} from '@elastic/eui';
import { useHistory } from 'react-router-dom';
// import { reactRouterNavigate } from '../../../../../../plugins/kibana_react/public';
import { MlCardState } from '../../../types';
import Exception from '@/components/Exception';
import {Button} from 'antd';
import Link from 'umi/link';
import {router} from 'umi';
export const EmptyState = ({
onRefresh,
navigateToApp,
docLinks,
getMlCardState,
canSave,
}: {
onRefresh: () => void;
navigateToApp: ApplicationStart['navigateToApp'];
docLinks: DocLinksStart;
getMlCardState: () => MlCardState;
canSave: boolean;
}) => {
const mlCard = (
<EuiFlexItem>
<EuiCard
onClick={() => navigateToApp('ml', { path: '#/filedatavisualizer' })}
className="inpEmptyState__card"
betaBadgeLabel={
getMlCardState() === MlCardState.ENABLED
? undefined
: 'Basic'
}
betaBadgeTooltipContent={ 'This feature requires a Basic license.'}
isDisabled={getMlCardState() === MlCardState.DISABLED}
icon={<EuiIcon size="xl" type="document" color="subdued" />}
title={
"Upload a file"
}
description={
"Import a CSV, NDJSON, or log file."
}
/>
</EuiFlexItem>
);
const createAnyway = (
<EuiText color="subdued" textAlign="center" size="xs">
Some indices may be hidden. Try to
<EuiLink
// {...reactRouterNavigate(useHistory(), 'create')}
data-test-subj="createAnyway">
create an index pattern
</EuiLink> anyway.
</EuiText>
);
const actions = (<div>
<Button type="primary" onClick={()=>{
router.push('/dev_tool');
}}></Button>
</div>)
return (
<>
<EuiPageContent
className="inpEmptyState"
grow={false}
horizontalPosition="center"
data-test-subj="indexPatternEmptyState"
>
<EuiPageContentHeader>
<EuiPageContentHeaderSection>
<EuiTitle>
<h2>
使
</h2>
</EuiTitle>
</EuiPageContentHeaderSection>
</EuiPageContentHeader>
<EuiSpacer size="m" />
<EuiPageContentBody>
{/* <EuiFlexGrid className="inpEmptyState__cardGrid" columns={3} responsive={true}>
<EuiFlexItem>
<EuiCard
className="inpEmptyState__card"
onClick={() => navigateToApp('home', { path: '#/tutorial_directory' })}
icon={<EuiIcon size="xl" type="database" color="subdued" />}
title={
"Add integration"
}
description={
"Add data from a variety of sources."
}
/>
</EuiFlexItem>
{getMlCardState() !== MlCardState.HIDDEN ? mlCard : <></>}
<EuiFlexItem>
<EuiCard
className="inpEmptyState__card"
onClick={() => navigateToApp('home', { path: '#/tutorial_directory/sampleData' })}
icon={<EuiIcon size="xl" type="heatmap" color="subdued" />}
title={
"Add sample data"
}
description={
"Load a data set and a Kibana dashboard."
}
/>
</EuiFlexItem>
</EuiFlexGrid> */}
<EuiSpacer size="xxl" />
<div className="inpEmptyState__footer">
<EuiFlexGrid columns={3}>
{/* <EuiFlexItem className="inpEmptyState__footerFlexItem">
<EuiDescriptionList
listItems={[
{
title: (
"了解更多?"
),
description: (
<EuiLink href={docLinks.links.addData} target="_blank" external>
Read documentation
</EuiLink>
),
},
]}
/>
</EuiFlexItem> */}
{/* <EuiFlexItem className="inpEmptyState__footerFlexItem">
<EuiDescriptionList
listItems={[
{
title: (
"确定已经有数据?"
),
description: (
<EuiLink onClick={onRefresh} data-test-subj="refreshIndicesButton">
{' '}
<EuiIcon type="refresh" size="s" />
</EuiLink>
),
},
]}
/>
</EuiFlexItem> */}
</EuiFlexGrid>
</div>
</EuiPageContentBody>
</EuiPageContent>
<EuiSpacer />
{/* {canSave && createAnyway} */}
</>
<Exception
type="404"
title="没有数据"
linkElement={Link}
desc={'当前集群找不到任何数据索引'}
actions={actions}
/>
);
};

View File

@ -32,17 +32,18 @@ import {
} from '@elastic/eui';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import React, { useState, useEffect } from 'react';
import { reactRouterNavigate, useKibana } from '../../../../kibana_react/public';
import { IndexPatternManagmentContext } from '../../types';
import { CreateButton } from '../create_button';
import { reactRouterNavigate } from '../../../../kibana_react/public';
import { IndexPatternTableItem, IndexPatternCreationOption } from '../types';
import { getIndexPatterns } from '../utils';
import { getListBreadcrumbs } from '../breadcrumbs';
import { EmptyState } from './empty_state';
import { MatchedItem, ResolveIndexResponseItemAlias } from '../create_index_pattern_wizard/types';
import { EmptyIndexPatternPrompt } from './empty_index_pattern_prompt';
import { getIndices } from '../create_index_pattern_wizard/lib';
import { useGlobalContext } from '../../context';
import {Button} from 'antd';
import PageHeaderWrapper from '@/components/PageHeaderWrapper';
import styles from '@/pages/System/Cluster/step.less';
import clusterBg from '@/assets/cluster_bg.png';
const pagination = {
initialPageSize: 10,
@ -65,10 +66,28 @@ const search = {
},
};
const ariaRegion = 'Index patterns';
const ariaRegion = '数据视图';
const title = '数据视图';
const content = (
<div className={styles.pageHeaderContent}>
<p>
Elasticsearch
</p>
</div>
);
const extraContent = (
<div className={styles.extraImg}>
<img
alt="数据视图"
src={clusterBg}
/>
</div>
);
interface Props extends RouteComponentProps {
canSave: boolean;
}
@ -79,9 +98,7 @@ export const IndexPatternTable = ({ canSave, history, selectedCluster }: Props)
savedObjects,
uiSettings,
indexPatternManagementStart,
chrome,
docLinks,
application,
http,
getMlCardState,
data,
@ -165,38 +182,17 @@ export const IndexPatternTable = ({ canSave, history, selectedCluster }: Props)
{
field: 'title',
name: '匹配规则',
// render: (
// name: string,
// index: {
// id: string;
// tags?: Array<{
// key: string;
// name: string;
// }>;
// }
// ) => (
// <>
// <EuiButtonEmpty style={{fontSize:14}} size="xs" {...reactRouterNavigate(history, `patterns/${index.id}`)}>
// {name}
// </EuiButtonEmpty>
// &emsp;
// <EuiBadgeGroup gutterSize="s">
// {index.tags &&
// index.tags.map(({ key: tagKey, name: tagName }) => (
// <EuiBadge key={tagKey}>{tagName}</EuiBadge>
// ))}
// </EuiBadgeGroup>
// </>
// ),
dataType: 'string' as const,
sortable: ({ sort }: { sort: string }) => sort,
},
];
const createButton = canSave ? (
<CreateButton options={creationOptions}>
<Button icon="plus" type="primary" onClick={()=>{
creationOptions[0].onClick();
}}>
{title}
</CreateButton>
</Button>
) : (
<></>
);
@ -210,43 +206,26 @@ export const IndexPatternTable = ({ canSave, history, selectedCluster }: Props)
if (!indexPatterns.length) {
if (!hasDataIndices && !remoteClustersExist) {
return (
<EmptyState
onRefresh={loadSources}
docLinks={docLinks}
// navigateToApp={application.navigateToApp}
getMlCardState={getMlCardState}
canSave={canSave}
/>
<EmptyState/>
);
} else {
return (
<EmptyIndexPatternPrompt
canSave={canSave}
creationOptions={creationOptions}
docLinksIndexPatternIntro={docLinks.links.indexPatterns.introduction}
setBreadcrumbs={setBreadcrumbs}
/>
);
}
}
const renderToolsRight = () => {
return [
createButton
];
};
return (
<PageHeaderWrapper title="数据视图" content={content} extraContent={extraContent}>
<EuiPageContent data-test-subj="indexPatternTable" role="region" aria-label={ariaRegion}>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiTitle>
<h2 style={{fontWeight:300}}>{title}</h2>
</EuiTitle>
<EuiSpacer size="s" />
<EuiText>
<p>
{title} Elasticsearch
</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>{createButton}</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<EuiContext i18n={{
mapping: {
'euiTablePagination.rowsPerPage': '每页行数',
@ -261,10 +240,14 @@ export const IndexPatternTable = ({ canSave, history, selectedCluster }: Props)
columns={columns}
pagination={pagination}
sorting={sorting}
search={search}
search={{
...search,
toolsRight: renderToolsRight(),
}}
/>
</EuiContext>
</EuiPageContent>
</PageHeaderWrapper>
);
};

View File

@ -27,10 +27,14 @@ class HeaderView extends PureComponent {
componentDidMount() {
document.addEventListener('scroll', this.handScroll, { passive: true });
this.fetchClusterStatus()
}
componentWillUnmount() {
document.removeEventListener('scroll', this.handScroll);
if(this.fetchClusterStatusTimer){
clearTimeout(this.fetchClusterStatusTimer);
}
}
getHeadWidth = () => {
@ -114,14 +118,13 @@ class HeaderView extends PureComponent {
this.ticking = false;
};
handleFetchClusterList = (from, size) => {
handleFetchClusterList = (name, size) => {
const { dispatch } = this.props;
return dispatch({
type: 'global/fetchClusterList',
payload: {
from,
size,
enabled: true,
name
}
});
};
@ -134,6 +137,20 @@ class HeaderView extends PureComponent {
});
}
fetchClusterStatus = async ()=>{
const {dispatch} = this.props;
const res = await dispatch({
type: 'global/fetchClusterStatus',
});
if(this.fetchClusterStatusTimer){
clearTimeout(this.fetchClusterStatusTimer);
}
if(!res){
return
}
this.fetchClusterStatusTimer = setTimeout(this.fetchClusterStatus, 10000);
}
render() {
const { isMobile, handleMenuCollapse, setting } = this.props;
const { navTheme, layout, fixedHeader } = setting;
@ -183,4 +200,5 @@ export default connect(({ user, global, setting, loading }) => ({
clusterVisible: global.clusterVisible,
clusterList: global.clusterList,
selectedCluster: global.selectedCluster,
clusterStatus: global.clusterStatus,
}))(HeaderView);

View File

@ -1,6 +1,6 @@
import { queryNotices } from '@/services/api';
import {message} from "antd";
import {searchClusterConfig} from "@/services/cluster";
import {searchClusterConfig, getClusterStatus} from "@/services/cluster";
import {formatESSearchResult, extractClusterIDFromURL} from '@/lib/elasticsearch/util';
import {Modal} from 'antd';
import router from "umi/router";
@ -16,6 +16,10 @@ export default {
clusterList: [],
selectedCluster: {name:"Select cluster", id: ""},
selectedClusterID: "",
search:{
cluster: {
}
}
},
effects: {
@ -48,7 +52,7 @@ export default {
return false;
}
res = formatESSearchResult(res)
let clusterList = yield select(state => state.global.clusterList);
let {clusterList, search} = yield select(state => state.global);
let data = res.data.filter(item=>item.enabled).map((item)=>{
return {
name: item.name,
@ -58,7 +62,7 @@ export default {
};
})
if(clusterList.length === 0){
if(clusterList.length === 0 && !payload.name){
if(data.length === 0 ){
Modal.info({
title: '系统提示',
@ -83,11 +87,20 @@ export default {
});
}
}
let newClusterList = [];
if(search.name != payload.name){
newClusterList = data;
}else{
newClusterList = clusterList.concat(data);
}
yield put({
type: 'saveData',
payload: {
clusterList: clusterList.concat(data),
clusterList: newClusterList,
search: {
...search,
cluster: payload,
}
}
})
return data;
@ -124,7 +137,24 @@ export default {
history.replace(newPath)
}
}
}
},
*fetchClusterStatus({payload}, {call, put}){
let res = yield call(getClusterStatus, payload);
if(!res){
return false
}
if(res.error){
console.log(res.error)
return false;
}
yield put({
type: 'saveData',
payload: {
clusterStatus: res
}
});
return res;
},
},
reducers: {

View File

@ -9,10 +9,31 @@ import {
CreateIndexPatternWizardWithRouter,
} from '../../components/kibana/index_pattern_management/public/components';
// import '@elastic/eui/dist/eui_theme_amsterdam_light.css';
import {useGlobalContext} from '../../components/kibana/index_pattern_management/public/context'
import {useGlobalContext} from '../../components/kibana/index_pattern_management/public/context';
import PageHeaderWrapper from '@/components/PageHeaderWrapper';
import styles from '../System/Cluster/step.less';
import clusterBg from '@/assets/cluster_bg.png';
import { connect } from 'dva';
const createContent = (
<div className={styles.pageHeaderContent}>
<p>
一个数据视图可以匹配单个索引, 比如, server-log-1, 或者 多个 索引, server-log-*.
</p>
</div>
);
const createExtraContent = (
<div className={styles.extraImg}>
<img
alt="创建视图"
src={clusterBg}
/>
</div>
);
const IndexPatterns = (props)=> {
const history = useMemo(()=>{
return new ScopedHistory(props.history, '/data/views');
@ -35,23 +56,27 @@ const IndexPatterns = (props)=> {
initFetch();
}, [props.selectedCluster]);
return (
<Router history={history}>
<Switch>
<Route path={['/create']} >
<CreateIndexPatternWizardWithRouter key={createComponentKey} />
</Route>
<Route path={['/patterns/:id/field/:fieldName', '/patterns/:id/create-field/']}>
<CreateEditFieldContainer selectedCluster={props.selectedCluster}/>
</Route>
<Route path={['/patterns/:id']} >
<EditIndexPatternContainer selectedCluster={props.selectedCluster}/>
</Route>
<Route path={['/']} >
<IndexPatternTableWithRouter canSave={true} selectedCluster={props.selectedCluster}/>
</Route>
</Switch>
</Router>
<Router history={history}>
<Switch>
<Route path={['/create']} >
<PageHeaderWrapper title="创建视图" content={createContent} extraContent={createExtraContent}>
<CreateIndexPatternWizardWithRouter key={createComponentKey} />
</PageHeaderWrapper>
</Route>
<Route path={['/patterns/:id/field/:fieldName', '/patterns/:id/create-field/']}>
<CreateEditFieldContainer selectedCluster={props.selectedCluster}/>
</Route>
<Route path={['/patterns/:id']} >
<EditIndexPatternContainer selectedCluster={props.selectedCluster}/>
</Route>
<Route path={['/']} >
<IndexPatternTableWithRouter canSave={true} selectedCluster={props.selectedCluster}/>
</Route>
</Switch>
</Router>
)
}

View File

@ -2,7 +2,7 @@ import React from 'react';
import {Button, Card, Col, Divider, Form, Input, Row, Table, Switch, Icon, Popconfirm, message} from "antd";
import Link from "umi/link";
import {connect} from "dva";
import {HealthStatusCircle} from './health_status';
import {HealthStatusCircle} from '@/components/infini/health_status_circle';
import PageHeaderWrapper from '@/components/PageHeaderWrapper';
import styles from './step.less';
import clusterBg from '@/assets/cluster_bg.png';
@ -25,8 +25,9 @@ const extraContent = (
);
@Form.create()
@connect(({clusterConfig}) =>({
clusterConfig
@connect(({clusterConfig, global}) =>({
clusterConfig,
clusterStatus: global.clusterStatus,
}))
class Index extends React.Component {
columns = [{
@ -38,12 +39,17 @@ class Index extends React.Component {
dataIndex: 'id',
key: 'health_status',
render: (val)=>{
const {clusterStatus} = this.props.clusterConfig;
const {clusterStatus} = this.props;
if(!clusterStatus || !clusterStatus[val]){
return
}
const isAvailable = clusterStatus[val].cluster_available;
if(!isAvailable){
return <Icon type="close-circle" style={{width:14, height:14, color:'red',borderRadius: 14, boxShadow: '0px 0px 5px #555'}}/>
}
const status = clusterStatus[val].health_status;
return <HealthStatusCircle status={status}/>
}
},{
title: '所属业务',
@ -79,7 +85,7 @@ class Index extends React.Component {
dataIndex: 'id',
key: 'mode_count',
render: (val)=>{
const {clusterStatus} = this.props.clusterConfig;
const {clusterStatus} = this.props;
if(!clusterStatus || !clusterStatus[val]){
return
}
@ -141,25 +147,6 @@ class Index extends React.Component {
}
componentDidMount() {
this.fetchData({})
this.fetchClusterStatus();
}
componentWillUnmount(){
if(this.fetchClusterStatusTimer){
clearTimeout(this.fetchClusterStatusTimer);
}
}
fetchClusterStatus = async ()=>{
const {dispatch} = this.props;
const res = await dispatch({
type: 'clusterConfig/fetchClusterStatus',
});
if(this.fetchClusterStatusTimer){
clearTimeout(this.fetchClusterStatusTimer);
}
if(!res){
return
}
this.fetchClusterStatusTimer = setTimeout(this.fetchClusterStatus, 10000);
}
handleSearchClick = ()=>{
@ -224,7 +211,7 @@ class Index extends React.Component {
return (
<PageHeaderWrapper title="集群管理" content={content} extraContent={extraContent}>
<Card>
<div style={{display:'flex', marginBottom:10, flex:"1 1 auto", justifyContent: 'space-between'}}>
<div style={{display:'flex', marginBottom:10, flex:"1 1 auto", justifyContent: 'space-between',alignItems:'center',}}>
<div>
<Form>
<Row gutter={{md:24, sm:16}}>

View File

@ -1,3 +0,0 @@
export const HealthStatusCircle = ({status})=>{
return <div style={{background: status, height:14, width:14, borderRadius: 14, boxShadow: '0px 0px 5px #555'}}></div>
}

View File

@ -1,5 +1,5 @@
import {createClusterConfig, searchClusterConfig, updateClusterConfig,deleteClusterConfig,
getClusterStatus, tryConnect} from "@/services/cluster";
tryConnect} from "@/services/cluster";
import {message} from "antd";
import {formatESSearchResult} from '@/lib/elasticsearch/util';
@ -10,20 +10,6 @@ export default {
editValue: {},
},
effects:{
*fetchClusterStatus({payload}, {call, put}){
let res = yield call(getClusterStatus, payload);
if(res.error){
message.error(res.error)
return false;
}
yield put({
type: 'saveData',
payload: {
clusterStatus: res
}
});
return res;
},
*fetchClusterList({payload}, {call, put, select}){
let res = yield call(searchClusterConfig, payload);
if(res.error){

View File

@ -1,5 +1,5 @@
import {Form, Input, Switch, Icon, InputNumber, Divider, Descriptions} from 'antd';
import {HealthStatusCircle} from '../health_status';
import {HealthStatusCircle} from '@/components/infini/health_status_circle';
@Form.create()
export class ExtraStep extends React.Component {

View File

@ -42,7 +42,9 @@ export async function searchClusterConfig(params) {
let url = `/elasticsearch/_search`;
let args = buildQueryArgs({
name: params.name,
enabled: params.enabled
enabled: params.enabled,
from: params.from,
size: params.size,
});
if(args.length > 0){
url += args;