feat(ui): add independent Flux Tasks page

pull/5868/head
Pavel Zavora 2022-02-16 21:18:05 +01:00
parent 6d9e333b32
commit cc34b79390
4 changed files with 285 additions and 0 deletions

View File

@ -30,6 +30,7 @@ import {LogsPage} from 'src/logs'
import AlertsApp from 'src/alerts'
import {
FluxTaskPage,
FluxTasksPage,
KapacitorPage,
KapacitorRulePage,
KapacitorRulesPage,
@ -188,6 +189,7 @@ class Root extends PureComponent<Record<string, never>, State> {
/>
<Route path="alerts" component={AlertsApp} />
<Route path="alert-rules" component={KapacitorRulesPage} />
<Route path="flux-tasks" component={FluxTasksPage} />
<Route
path="kapacitors/:kid/alert-rules/:ruleID" // ruleID can be "new"
component={KapacitorRulePage}

View File

@ -0,0 +1,150 @@
import React, {useEffect, useState} from 'react'
import {FluxTask, Kapacitor, Source} from 'src/types'
import KapacitorScopedPage from './KapacitorScopedPage'
import {useDispatch} from 'react-redux'
import {deleteFluxTask, getFluxTasks, updateFluxTaskStatus} from '../apis'
import errorMessage from '../utils/errorMessage'
import PageSpinner from 'src/shared/components/PageSpinner'
import FluxTasksTable from '../components/FluxTasksTable'
import {notify} from 'src/shared/actions/notifications'
import {
notifyAlertRuleDeleted,
notifyAlertRuleDeleteFailed,
notifyFluxTaskStatusUpdated,
notifyFluxTaskStatusUpdateFailed,
} from 'src/shared/copy/notifications'
import useDebounce from '../../utils/useDebounce'
const Contents = ({
kapacitor,
source,
}: {
kapacitor: Kapacitor
source: Source
}) => {
const [loading, setLoading] = useState(true)
const [nameFilter, setNameFilter] = useState('')
const [reloadRequired, setReloadRequired] = useState(0)
const [error, setError] = useState(undefined)
const [list, setList] = useState<FluxTask[] | null>(null)
const dispatch = useDispatch()
const filter = useDebounce(nameFilter)
useEffect(() => {
setLoading(true)
const fetchData = async () => {
try {
let data = (await getFluxTasks(kapacitor)) as FluxTask[]
if (data && filter) {
data = data.filter(x => x.name.includes(filter))
}
setList(data)
} catch (e) {
if (e.status === 404) {
setList(null)
} else {
console.error(e)
setError(
new Error(
e?.data?.message
? e.data.message
: `Cannot load flux task: ${errorMessage(e)}`
)
)
}
} finally {
setLoading(false)
}
}
fetchData()
}, [kapacitor, reloadRequired, filter])
if (error) {
return (
<div className="panel panel-solid">
<div className="panel-body">
<p className="unexpected_error">{error.toString()}</p>
</div>
</div>
)
}
if (list === null) {
return (
<div className="panel panel-solid">
<div className="panel-body">
<p className="unexpected_error">
No flux tasks are available. Kapacitor 1.6+ is required with Flux
tasks enabled.
</p>
</div>
</div>
)
}
return (
<div className="panel">
<div className="panel-heading">
<div className="search-widget" style={{flexGrow: 1}}>
<input
type="text"
className="form-control input-sm"
placeholder="Search name"
value={nameFilter}
onChange={e => {
setNameFilter(e.target.value)
}}
/>
<span className="icon search" />
</div>
</div>
<div className="panel-body">
{loading ? (
<PageSpinner />
) : (
<FluxTasksTable
kapacitorLink={`/sources/${source.id}/kapacitors/${kapacitor.id}`}
tasks={list}
onDelete={(task: FluxTask) => {
deleteFluxTask(kapacitor, task)
.then(() => {
setReloadRequired(reloadRequired + 1)
dispatch(notify(notifyAlertRuleDeleted(task.name)))
})
.catch(() => {
dispatch(notify(notifyAlertRuleDeleteFailed(task.name)))
})
}}
onChangeTaskStatus={(task: FluxTask) => {
const status = task.status === 'active' ? 'inactive' : 'active'
updateFluxTaskStatus(kapacitor, task, status)
.then(() => {
setList(
list.map(x => (x.id === task.id ? {...task, status} : x))
)
dispatch(
notify(notifyFluxTaskStatusUpdated(task.name, status))
)
})
.catch(() => {
dispatch(
notify(notifyFluxTaskStatusUpdateFailed(task.name, status))
)
return false
})
}}
/>
)}
</div>
</div>
)
}
const FluxTasksPage = ({source: src}: {source: Source}) => {
return (
<KapacitorScopedPage source={src} title="Flux Tasks">
{(kapacitor: Kapacitor, source: Source) => (
<Contents kapacitor={kapacitor} source={source} />
)}
</KapacitorScopedPage>
)
}
export default FluxTasksPage

View File

@ -0,0 +1,131 @@
// Libraries
import React, {PureComponent} from 'react'
import {connect} from 'react-redux'
// APIs
import {getKapacitors, pingKapacitor} from 'src/shared/apis'
// Utils
import {notifyKapacitorConnectionFailed} from 'src/shared/copy/notifications'
// Actions
import {notify as notifyAction} from 'src/shared/actions/notifications'
import {setActiveKapacitorAsync} from 'src/shared/actions/sources'
// Components
import QuestionMarkTooltip from 'src/shared/components/QuestionMarkTooltip'
import {Page, Spinner} from 'src/reusable_ui'
import Dropdown from 'src/reusable_ui/components/dropdowns/Dropdown'
// Types
import {Source, Kapacitor, RemoteDataState} from 'src/types'
// Decorators
import {ErrorHandling} from 'src/shared/decorators/errors'
import NoKapacitorError from 'src/shared/components/NoKapacitorError'
interface Props {
// connected props
notify: typeof mdtp.notify
setActiveKapacitor: typeof mdtp.setActiveKapacitor
// owen props
title: string
source: Source
tooltip?: string
children: (kapacitor: Kapacitor, source: Source) => JSX.Element
}
interface State {
loading: RemoteDataState
kapacitors?: Kapacitor[]
kapacitor?: Kapacitor
error?: Error
}
@ErrorHandling
export class KapacitorScopedPage extends PureComponent<Props, State> {
constructor(props: Props) {
super(props)
this.state = {
loading: RemoteDataState.NotStarted,
}
}
public componentDidMount() {
const {source} = this.props
this.setState({loading: RemoteDataState.Loading})
getKapacitors(source).then(kapacitors => {
const kapacitor =
kapacitors && kapacitors.length
? kapacitors.find(x => x.active) || kapacitors[0]
: undefined
this.setState({kapacitors, kapacitor, loading: RemoteDataState.Done})
})
}
public render() {
const {tooltip, title, source, children} = this.props
const {loading, kapacitor, kapacitors} = this.state
return (
<Page className={kapacitor ? '' : 'empty-tasks-page'}>
<Page.Header>
<Page.Header.Left>
<Page.Title title={kapacitor ? `${title} on` : title} />
{kapacitor ? (
<Dropdown
customClass="kapacitor-switcher"
onChange={this.handleSetActiveKapacitor}
widthPixels={330}
selectedID={kapacitor.id}
>
{kapacitors.map(k => (
<Dropdown.Item key={k.id} id={k.id} value={k.id}>
{`${k.name} @ ${k.url}`}
</Dropdown.Item>
))}
</Dropdown>
) : undefined}
</Page.Header.Left>
<Page.Header.Right showSourceIndicator={true}>
{tooltip ? (
<QuestionMarkTooltip
tipID="manage-tasks--tooltip"
tipContent={tooltip}
/>
) : undefined}
</Page.Header.Right>
</Page.Header>
<Page.Contents>
<Spinner loading={loading}>
{kapacitor ? (
children(kapacitor, source)
) : (
<NoKapacitorError source={source} />
)}
</Spinner>
</Page.Contents>
</Page>
)
}
private handleSetActiveKapacitor = async (
kapacitorID: string
): Promise<void> => {
const {setActiveKapacitor} = this.props
const {kapacitors} = this.state
const toKapacitor = kapacitors.find(k => k.id === kapacitorID)
setActiveKapacitor(toKapacitor)
pingKapacitor(toKapacitor).catch(() => {
this.props.notify(notifyKapacitorConnectionFailed())
})
}
}
const mdtp = {
notify: notifyAction,
setActiveKapacitor: setActiveKapacitorAsync,
}
export default connect(null, mdtp)(KapacitorScopedPage)

View File

@ -3,6 +3,7 @@ import KapacitorRulePage from './containers/KapacitorRulePage'
import KapacitorRulesPage from './containers/KapacitorRulesPage'
import TickscriptPage from './containers/TickscriptPage'
import FluxTaskPage from './containers/FluxTaskPage'
import FluxTasksPage from './containers/FluxTasksPage'
export {
KapacitorPage,
@ -10,4 +11,5 @@ export {
KapacitorRulesPage,
TickscriptPage,
FluxTaskPage,
FluxTasksPage,
}