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

View File

@ -14,6 +14,11 @@ const config = {
title: '500', title: '500',
desc: '抱歉,服务器出错了', desc: '抱歉,服务器出错了',
}, },
empty: {
img: 'https://gw.alipayobjects.com/zos/rmsportal/KpnpchXsobRgLElEozzI.svg',
title: '找不到数据',
desc: '当前集群找到相关数据',
}
}; };
export default config; 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 React from 'react';
import InfiniteScroll from 'react-infinite-scroller'; import InfiniteScroll from 'react-infinite-scroller';
import styles from './DropdownSelect.less'; import styles from './DropdownSelect.less';
import _ from "lodash"; import _ from "lodash";
import {DropdownItem} from './DropdownItem';
import {HealthStatusCircle} from '@/components/infini/health_status_circle'
class DropdownSelect extends React.Component{ class DropdownSelect extends React.Component{
state={ state={
value: this.props.defaultValue, value: this.props.defaultValue,
loading: false, loading: false,
hasMore: true, hasMore: true,
overlayVisible: false,
} }
handleItemClick = (item)=>{ handleItemClick = (item)=>{
let preValue = this.props.value || this.state.value; let preValue = this.props.value || this.state.value;
this.setState({ this.setState({
value: item, value: item,
overlayVisible: false,
},()=>{ },()=>{
let onChange = this.props.onChange; let onChange = this.props.onChange;
if(preValue != item && onChange && typeof onChange == 'function'){ if(preValue != item && onChange && typeof onChange == 'function'){
@ -35,75 +39,74 @@ class DropdownSelect extends React.Component{
}) })
}) })
} }
fetchData = ()=>{ fetchData = (name)=>{
let me = this; let me = this;
const {fetchData, size} = this.props; const {fetchData, size} = this.props;
let data = this.props.data || []; let data = this.props.data || [];
let from = data.length; return fetchData(name || '', size);
return fetchData(from, size);
} }
handleInfiniteOnLoad = (page) => { handleInfiniteOnLoad = (name) => {
let { data } = this.props; let { data } = this.props;
this.setState({ this.setState({
loading: true, loading: true,
}) })
this.fetchData().then((newdata)=>{ this.fetchData(name).then((newdata)=>{
let newState = { let newState = {
loading: false, loading: false,
}; };
if(newdata.length < this.props.size){ if(newdata.length < this.props.size){
message.info("no more data"); //message.info("no more data");
newState.hasMore = false; newState.hasMore = false;
} }
this.setState(newState); this.setState(newState);
}); });
} }
handleInputChange = (e) =>{
const name = e.target.value;
this.setState({
displayValue: name,
})
this.handleInfiniteOnLoad(name);
}
render(){ render(){
let me = this; let me = this;
const {labelField} = this.props; const {labelField, clusterStatus} = this.props;
let value = this.props.value || this.state.value; let value = this.props.value || this.state.value;
let displayVaue = value[labelField];
const menu = (<div className={styles.dropmenu} style={{width: this.props.width}}> const menu = (<div className={styles.dropmenu} style={{width: this.props.width}}>
<div className={styles.infiniteContainer} style={{height: this.props.height}}> <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 <InfiniteScroll
initialLoad={false} initialLoad={false}
loadMore={this.handleInfiniteOnLoad} loadMore={this.handleInfiniteOnLoad}
hasMore={!this.state.loading && this.state.hasMore} hasMore={!this.state.loading && this.state.hasMore}
useWindow={false} 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}> <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.props.data || []).map((item)=>{
return <div className={styles.item}><Button key={item[labelField]} onClick={() => { // 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) this.handleItemClick(item)
}} }}
className={_.isEqual(item, value) ? styles.btnitem + " " + styles.selected : styles.btnitem}>{item[labelField]}</Button></div> />
})} })}
</div> </div>
</InfiniteScroll> </InfiniteScroll>
@ -114,11 +117,23 @@ class DropdownSelect extends React.Component{
</div> </div>
)} )}
</div>); </div>);
const cstatus = clusterStatus ? clusterStatus[value?.id] : null;
return( return(
this.props.visible ? this.props.visible ?
(<Dropdown overlay={menu} placement="bottomLeft"> (<Dropdown overlay={menu} placement="bottomLeft" visible={this.state.overlayVisible}
<Button className={styles['btn-ds']}>{value[labelField]} <Icon style={{float: 'right', marginTop: 3}} onVisibleChange={(flag)=>{
type="caret-down"/></Button> 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>) : "" </Dropdown>) : ""
) )
} }

View File

@ -9,6 +9,7 @@
margin-left: 15px; margin-left: 15px;
position: relative; position: relative;
bottom: 8px; bottom: 8px;
height: 32px;
span{ span{
color: #333; color: #333;
} }
@ -16,7 +17,7 @@
.dropmenu{ .dropmenu{
box-shadow: 0 0 15px 0 rgba(0, 0, 0, .15); box-shadow: 0 0 15px 0 rgba(0, 0, 0, .15);
padding: 20px; padding: 0px;
padding-bottom: 4px; padding-bottom: 4px;
width: 500px; width: 500px;
background: #fff; background: #fff;
@ -57,3 +58,27 @@
display: inline-block; display: inline-block;
width: 100%; 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 React, { PureComponent } from 'react';
import { Icon } from 'antd'; import { Icon, Select } from 'antd';
import Link from 'umi/link'; import Link from 'umi/link';
import Debounce from 'lodash-decorators/debounce'; import Debounce from 'lodash-decorators/debounce';
import styles from './index.less'; import styles from './index.less';
@ -53,6 +53,7 @@ export default class GlobalHeader extends PureComponent {
onClick={this.toggle} onClick={this.toggle}
/> />
<DropdownSelect defaultValue={selectedCluster} <DropdownSelect defaultValue={selectedCluster}
clusterStatus={this.props.clusterStatus}
value={selectedCluster} value={selectedCluster}
labelField="name" labelField="name"
visible={clusterVisible} 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) { async delete(indexPatternId: string) {
indexPatternCache.clear(indexPatternId); indexPatternCache.clear(indexPatternId);
return this.savedObjectsClient.delete(savedObjectType, 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 { areRefreshIntervalsDifferent, areTimeRangesDifferent } from './lib/diff_time_picker_vals';
import { getForceNow } from './lib/get_force_now'; import { getForceNow } from './lib/get_force_now';
import { TimefilterConfig, InputTimeRange, TimeRangeBounds } from './types'; import { TimefilterConfig, InputTimeRange, TimeRangeBounds } from './types';
import { getTime, RefreshInterval, TimeRange } from '../../../common'; import { RefreshInterval, TimeRange } from '../../../common';
import { calculateBounds } from '../../../common/query/timefilter/get_time'; import {getTime, calculateBounds } from '../../../common/query/timefilter/get_time';
import { TimeHistoryContract } from './time_history'; import { TimeHistoryContract } from './time_history';
import { IndexPattern } from '../../index_patterns'; import { IndexPattern } from '../../index_patterns';

View File

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

View File

@ -18,76 +18,32 @@
*/ */
import './empty_index_pattern_prompt.scss'; 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 { 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 Link from 'umi/link';
import Exception from '@/components/Exception'; import Exception from '@/components/Exception';
import {Button} from 'antd';
interface Props { interface Props {
canSave: boolean;
creationOptions: IndexPatternCreationOption[]; creationOptions: IndexPatternCreationOption[];
docLinksIndexPatternIntro: string;
setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs'];
} }
export const EmptyIndexPatternPrompt = ({ export const EmptyIndexPatternPrompt = ({
canSave,
creationOptions, creationOptions,
docLinksIndexPatternIntro,
setBreadcrumbs,
}: Props) => { }: Props) => {
setBreadcrumbs(getListBreadcrumbs()); const actions = (<div>
<Button type="primary" onClick={()=>{
creationOptions[0].onClick();
}}></Button>
</div>)
return ( return (
<EuiPageContent <Exception
data-test-subj="emptyIndexPatternPrompt" type="404"
className="inpEmptyIndexPatternPrompt" title="没有视图"
grow={false} linkElement={Link}
horizontalPosition="center" desc={'当前集群找不到任何数据视图'}
> actions={actions}
<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>
); );
}; };

View File

@ -17,165 +17,28 @@
* under the License. * under the License.
*/ */
import './empty_state.scss'; import Exception from '@/components/Exception';
import React from 'react'; import {Button} from 'antd';
import { DocLinksStart, ApplicationStart } from 'kibana/public'; import Link from 'umi/link';
import { import {router} from 'umi';
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';
export const EmptyState = ({ 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 = ( const actions = (<div>
<EuiText color="subdued" textAlign="center" size="xs"> <Button type="primary" onClick={()=>{
Some indices may be hidden. Try to router.push('/dev_tool');
<EuiLink }}></Button>
// {...reactRouterNavigate(useHistory(), 'create')} </div>)
data-test-subj="createAnyway">
create an index pattern
</EuiLink> anyway.
</EuiText>
);
return ( return (
<> <Exception
<EuiPageContent type="404"
className="inpEmptyState" title="没有数据"
grow={false} linkElement={Link}
horizontalPosition="center" desc={'当前集群找不到任何数据索引'}
data-test-subj="indexPatternEmptyState" actions={actions}
>
<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} */}
</>
); );
}; };

View File

@ -32,17 +32,18 @@ import {
} from '@elastic/eui'; } from '@elastic/eui';
import { withRouter, RouteComponentProps } from 'react-router-dom'; import { withRouter, RouteComponentProps } from 'react-router-dom';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { reactRouterNavigate, useKibana } from '../../../../kibana_react/public'; import { reactRouterNavigate } from '../../../../kibana_react/public';
import { IndexPatternManagmentContext } from '../../types';
import { CreateButton } from '../create_button';
import { IndexPatternTableItem, IndexPatternCreationOption } from '../types'; import { IndexPatternTableItem, IndexPatternCreationOption } from '../types';
import { getIndexPatterns } from '../utils'; import { getIndexPatterns } from '../utils';
import { getListBreadcrumbs } from '../breadcrumbs';
import { EmptyState } from './empty_state'; import { EmptyState } from './empty_state';
import { MatchedItem, ResolveIndexResponseItemAlias } from '../create_index_pattern_wizard/types'; import { MatchedItem, ResolveIndexResponseItemAlias } from '../create_index_pattern_wizard/types';
import { EmptyIndexPatternPrompt } from './empty_index_pattern_prompt'; import { EmptyIndexPatternPrompt } from './empty_index_pattern_prompt';
import { getIndices } from '../create_index_pattern_wizard/lib'; import { getIndices } from '../create_index_pattern_wizard/lib';
import { useGlobalContext } from '../../context'; 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 = { const pagination = {
initialPageSize: 10, initialPageSize: 10,
@ -65,10 +66,28 @@ const search = {
}, },
}; };
const ariaRegion = 'Index patterns';
const ariaRegion = '数据视图';
const title = '数据视图'; 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 { interface Props extends RouteComponentProps {
canSave: boolean; canSave: boolean;
} }
@ -79,9 +98,7 @@ export const IndexPatternTable = ({ canSave, history, selectedCluster }: Props)
savedObjects, savedObjects,
uiSettings, uiSettings,
indexPatternManagementStart, indexPatternManagementStart,
chrome,
docLinks, docLinks,
application,
http, http,
getMlCardState, getMlCardState,
data, data,
@ -165,38 +182,17 @@ export const IndexPatternTable = ({ canSave, history, selectedCluster }: Props)
{ {
field: 'title', field: 'title',
name: '匹配规则', 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, dataType: 'string' as const,
sortable: ({ sort }: { sort: string }) => sort, sortable: ({ sort }: { sort: string }) => sort,
}, },
]; ];
const createButton = canSave ? ( const createButton = canSave ? (
<CreateButton options={creationOptions}> <Button icon="plus" type="primary" onClick={()=>{
creationOptions[0].onClick();
}}>
{title} {title}
</CreateButton> </Button>
) : ( ) : (
<></> <></>
); );
@ -210,43 +206,26 @@ export const IndexPatternTable = ({ canSave, history, selectedCluster }: Props)
if (!indexPatterns.length) { if (!indexPatterns.length) {
if (!hasDataIndices && !remoteClustersExist) { if (!hasDataIndices && !remoteClustersExist) {
return ( return (
<EmptyState <EmptyState/>
onRefresh={loadSources}
docLinks={docLinks}
// navigateToApp={application.navigateToApp}
getMlCardState={getMlCardState}
canSave={canSave}
/>
); );
} else { } else {
return ( return (
<EmptyIndexPatternPrompt <EmptyIndexPatternPrompt
canSave={canSave}
creationOptions={creationOptions} creationOptions={creationOptions}
docLinksIndexPatternIntro={docLinks.links.indexPatterns.introduction}
setBreadcrumbs={setBreadcrumbs}
/> />
); );
} }
} }
const renderToolsRight = () => {
return [
createButton
];
};
return ( return (
<PageHeaderWrapper title="数据视图" content={content} extraContent={extraContent}>
<EuiPageContent data-test-subj="indexPatternTable" role="region" aria-label={ariaRegion}> <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={{ <EuiContext i18n={{
mapping: { mapping: {
'euiTablePagination.rowsPerPage': '每页行数', 'euiTablePagination.rowsPerPage': '每页行数',
@ -261,10 +240,14 @@ export const IndexPatternTable = ({ canSave, history, selectedCluster }: Props)
columns={columns} columns={columns}
pagination={pagination} pagination={pagination}
sorting={sorting} sorting={sorting}
search={search} search={{
...search,
toolsRight: renderToolsRight(),
}}
/> />
</EuiContext> </EuiContext>
</EuiPageContent> </EuiPageContent>
</PageHeaderWrapper>
); );
}; };

View File

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

View File

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

View File

@ -9,10 +9,31 @@ import {
CreateIndexPatternWizardWithRouter, CreateIndexPatternWizardWithRouter,
} from '../../components/kibana/index_pattern_management/public/components'; } from '../../components/kibana/index_pattern_management/public/components';
// import '@elastic/eui/dist/eui_theme_amsterdam_light.css'; // 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'; 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 IndexPatterns = (props)=> {
const history = useMemo(()=>{ const history = useMemo(()=>{
return new ScopedHistory(props.history, '/data/views'); return new ScopedHistory(props.history, '/data/views');
@ -35,11 +56,15 @@ const IndexPatterns = (props)=> {
initFetch(); initFetch();
}, [props.selectedCluster]); }, [props.selectedCluster]);
return ( return (
<Router history={history}> <Router history={history}>
<Switch> <Switch>
<Route path={['/create']} > <Route path={['/create']} >
<PageHeaderWrapper title="创建视图" content={createContent} extraContent={createExtraContent}>
<CreateIndexPatternWizardWithRouter key={createComponentKey} /> <CreateIndexPatternWizardWithRouter key={createComponentKey} />
</PageHeaderWrapper>
</Route> </Route>
<Route path={['/patterns/:id/field/:fieldName', '/patterns/:id/create-field/']}> <Route path={['/patterns/:id/field/:fieldName', '/patterns/:id/create-field/']}>
<CreateEditFieldContainer selectedCluster={props.selectedCluster}/> <CreateEditFieldContainer selectedCluster={props.selectedCluster}/>

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 {Button, Card, Col, Divider, Form, Input, Row, Table, Switch, Icon, Popconfirm, message} from "antd";
import Link from "umi/link"; import Link from "umi/link";
import {connect} from "dva"; import {connect} from "dva";
import {HealthStatusCircle} from './health_status'; import {HealthStatusCircle} from '@/components/infini/health_status_circle';
import PageHeaderWrapper from '@/components/PageHeaderWrapper'; import PageHeaderWrapper from '@/components/PageHeaderWrapper';
import styles from './step.less'; import styles from './step.less';
import clusterBg from '@/assets/cluster_bg.png'; import clusterBg from '@/assets/cluster_bg.png';
@ -25,8 +25,9 @@ const extraContent = (
); );
@Form.create() @Form.create()
@connect(({clusterConfig}) =>({ @connect(({clusterConfig, global}) =>({
clusterConfig clusterConfig,
clusterStatus: global.clusterStatus,
})) }))
class Index extends React.Component { class Index extends React.Component {
columns = [{ columns = [{
@ -38,12 +39,17 @@ class Index extends React.Component {
dataIndex: 'id', dataIndex: 'id',
key: 'health_status', key: 'health_status',
render: (val)=>{ render: (val)=>{
const {clusterStatus} = this.props.clusterConfig; const {clusterStatus} = this.props;
if(!clusterStatus || !clusterStatus[val]){ if(!clusterStatus || !clusterStatus[val]){
return 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; const status = clusterStatus[val].health_status;
return <HealthStatusCircle status={status}/> return <HealthStatusCircle status={status}/>
} }
},{ },{
title: '所属业务', title: '所属业务',
@ -79,7 +85,7 @@ class Index extends React.Component {
dataIndex: 'id', dataIndex: 'id',
key: 'mode_count', key: 'mode_count',
render: (val)=>{ render: (val)=>{
const {clusterStatus} = this.props.clusterConfig; const {clusterStatus} = this.props;
if(!clusterStatus || !clusterStatus[val]){ if(!clusterStatus || !clusterStatus[val]){
return return
} }
@ -141,25 +147,6 @@ class Index extends React.Component {
} }
componentDidMount() { componentDidMount() {
this.fetchData({}) 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 = ()=>{ handleSearchClick = ()=>{
@ -224,7 +211,7 @@ class Index extends React.Component {
return ( return (
<PageHeaderWrapper title="集群管理" content={content} extraContent={extraContent}> <PageHeaderWrapper title="集群管理" content={content} extraContent={extraContent}>
<Card> <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> <div>
<Form> <Form>
<Row gutter={{md:24, sm:16}}> <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, import {createClusterConfig, searchClusterConfig, updateClusterConfig,deleteClusterConfig,
getClusterStatus, tryConnect} from "@/services/cluster"; tryConnect} from "@/services/cluster";
import {message} from "antd"; import {message} from "antd";
import {formatESSearchResult} from '@/lib/elasticsearch/util'; import {formatESSearchResult} from '@/lib/elasticsearch/util';
@ -10,20 +10,6 @@ export default {
editValue: {}, editValue: {},
}, },
effects:{ 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}){ *fetchClusterList({payload}, {call, put, select}){
let res = yield call(searchClusterConfig, payload); let res = yield call(searchClusterConfig, payload);
if(res.error){ if(res.error){

View File

@ -1,5 +1,5 @@
import {Form, Input, Switch, Icon, InputNumber, Divider, Descriptions} from 'antd'; 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() @Form.create()
export class ExtraStep extends React.Component { export class ExtraStep extends React.Component {

View File

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