mirror of https://github.com/grafana/grafana
Live: remove admin pages, add alpha panel (#28101)
parent
e69fe93e85
commit
2567e5202a
@ -1,202 +0,0 @@ |
||||
import React, { PureComponent } from 'react'; |
||||
import { hot } from 'react-hot-loader'; |
||||
import { connect } from 'react-redux'; |
||||
import { css } from 'emotion'; |
||||
import { StoreState } from 'app/types'; |
||||
import { getNavModel } from 'app/core/selectors/navModel'; |
||||
import Page from 'app/core/components/Page/Page'; |
||||
import { |
||||
NavModel, |
||||
SelectableValue, |
||||
FeatureState, |
||||
LiveChannelScope, |
||||
LiveChannelConfig, |
||||
LiveChannelSupport, |
||||
} from '@grafana/data'; |
||||
import { LivePanel } from './LivePanel'; |
||||
import { Select, FeatureInfoBox, Container } from '@grafana/ui'; |
||||
import { getGrafanaLiveCentrifugeSrv } from '../live/live'; |
||||
|
||||
interface Props { |
||||
navModel: NavModel; |
||||
} |
||||
|
||||
const scopes: Array<SelectableValue<LiveChannelScope>> = [ |
||||
{ label: 'Grafana', value: LiveChannelScope.Grafana, description: 'Core grafana live features' }, |
||||
{ label: 'Data Sources', value: LiveChannelScope.DataSource, description: 'Data sources with live support' }, |
||||
{ label: 'Plugins', value: LiveChannelScope.Plugin, description: 'Plugins with live support' }, |
||||
]; |
||||
|
||||
interface State { |
||||
scope: LiveChannelScope; |
||||
namespace?: string; |
||||
path?: string; |
||||
|
||||
namespaces: Array<SelectableValue<string>>; |
||||
paths: Array<SelectableValue<string>>; |
||||
support?: LiveChannelSupport; |
||||
config?: LiveChannelConfig; |
||||
} |
||||
|
||||
export class LiveAdmin extends PureComponent<Props, State> { |
||||
state: State = { |
||||
scope: LiveChannelScope.Grafana, |
||||
namespace: 'testdata', |
||||
path: 'random-2s-stream', |
||||
namespaces: [], |
||||
paths: [], |
||||
}; |
||||
// onTextChanged: ((event: FormEvent<HTMLInputElement>) => void) | undefined;
|
||||
// onPublish: ((event: MouseEvent<HTMLButtonElement, MouseEvent>) => void) | undefined;
|
||||
|
||||
async componentDidMount() { |
||||
const { scope, namespace, path } = this.state; |
||||
const srv = getGrafanaLiveCentrifugeSrv(); |
||||
const namespaces = await srv.scopes[scope].listNamespaces(); |
||||
const support = namespace ? await srv.scopes[scope].getChannelSupport(namespace) : undefined; |
||||
const paths = support ? await support.getSupportedPaths() : undefined; |
||||
const config = support && path ? await support.getChannelConfig(path) : undefined; |
||||
|
||||
this.setState({ |
||||
namespaces, |
||||
support, |
||||
paths: paths |
||||
? paths.map(p => ({ |
||||
label: p.path, |
||||
value: p.path, |
||||
description: p.description, |
||||
})) |
||||
: [], |
||||
config, |
||||
}); |
||||
} |
||||
|
||||
onScopeChanged = async (v: SelectableValue<LiveChannelScope>) => { |
||||
if (v.value) { |
||||
const srv = getGrafanaLiveCentrifugeSrv(); |
||||
|
||||
this.setState({ |
||||
scope: v.value, |
||||
namespace: undefined, |
||||
path: undefined, |
||||
namespaces: await srv.scopes[v.value!].listNamespaces(), |
||||
paths: [], |
||||
support: undefined, |
||||
config: undefined, |
||||
}); |
||||
} |
||||
}; |
||||
|
||||
onNamespaceChanged = async (v: SelectableValue<string>) => { |
||||
if (v.value) { |
||||
const namespace = v.value; |
||||
const srv = getGrafanaLiveCentrifugeSrv(); |
||||
const support = await srv.scopes[this.state.scope].getChannelSupport(namespace); |
||||
|
||||
this.setState({ |
||||
namespace: v.value, |
||||
paths: support!.getSupportedPaths().map(p => ({ |
||||
label: p.path, |
||||
value: p.path, |
||||
description: p.description, |
||||
})), |
||||
path: undefined, |
||||
config: undefined, |
||||
}); |
||||
} |
||||
}; |
||||
|
||||
onPathChanged = async (v: SelectableValue<string>) => { |
||||
if (v.value) { |
||||
const path = v.value; |
||||
const srv = getGrafanaLiveCentrifugeSrv(); |
||||
const support = await srv.scopes[this.state.scope].getChannelSupport(this.state.namespace!); |
||||
if (!support) { |
||||
this.setState({ |
||||
namespace: undefined, |
||||
paths: [], |
||||
config: undefined, |
||||
support, |
||||
}); |
||||
return; |
||||
} |
||||
|
||||
this.setState({ |
||||
path, |
||||
support, |
||||
config: support.getChannelConfig(path), |
||||
}); |
||||
} |
||||
}; |
||||
|
||||
render() { |
||||
const { navModel } = this.props; |
||||
const { scope, namespace, namespaces, path, paths, config } = this.state; |
||||
|
||||
return ( |
||||
<Page navModel={navModel}> |
||||
<Page.Contents> |
||||
<Container grow={1}> |
||||
<FeatureInfoBox |
||||
title="Grafana Live" |
||||
featureState={FeatureState.alpha} |
||||
// url={getDocsLink(DocsId.Transformations)}
|
||||
> |
||||
<p> |
||||
This supports real-time event streams in grafana core. This feature is under heavy development. Expect |
||||
the intefaces and structures to change as this becomes more production ready. |
||||
</p> |
||||
</FeatureInfoBox> |
||||
<br /> |
||||
<br /> |
||||
</Container> |
||||
|
||||
<div |
||||
className={css` |
||||
width: 100%; |
||||
display: flex; |
||||
> div { |
||||
margin-right: 8px; |
||||
min-width: 150px; |
||||
} |
||||
`}
|
||||
> |
||||
<div> |
||||
<h5>Scope</h5> |
||||
<Select options={scopes} value={scopes.find(s => s.value === scope)} onChange={this.onScopeChanged} /> |
||||
</div> |
||||
<div> |
||||
<h5>Namespace</h5> |
||||
<Select |
||||
options={namespaces} |
||||
value={namespaces.find(s => s.value === namespace) || namespace || ''} |
||||
onChange={this.onNamespaceChanged} |
||||
allowCustomValue={true} |
||||
backspaceRemovesValue={true} |
||||
/> |
||||
</div> |
||||
<div> |
||||
<h5>Path</h5> |
||||
<Select |
||||
options={paths} |
||||
value={paths.find(s => s.value === path) || path || ''} |
||||
onChange={this.onPathChanged} |
||||
allowCustomValue={true} |
||||
backspaceRemovesValue={true} |
||||
/> |
||||
</div> |
||||
</div> |
||||
<br /> |
||||
<br /> |
||||
{scope && namespace && path && <LivePanel scope={scope} namespace={namespace} path={path} config={config} />} |
||||
</Page.Contents> |
||||
</Page> |
||||
); |
||||
} |
||||
} |
||||
|
||||
const mapStateToProps = (state: StoreState) => ({ |
||||
navModel: getNavModel(state.navIndex, 'live'), |
||||
}); |
||||
|
||||
export default hot(module)(connect(mapStateToProps)(LiveAdmin)); |
@ -1,150 +0,0 @@ |
||||
import React, { PureComponent } from 'react'; |
||||
import { Unsubscribable, PartialObserver } from 'rxjs'; |
||||
import { getGrafanaLiveSrv } from '@grafana/runtime'; |
||||
import { |
||||
AppEvents, |
||||
isLiveChannelStatusEvent, |
||||
LiveChannel, |
||||
LiveChannelConfig, |
||||
LiveChannelConnectionState, |
||||
LiveChannelEvent, |
||||
LiveChannelEventType, |
||||
LiveChannelScope, |
||||
LiveChannelStatusEvent, |
||||
} from '@grafana/data'; |
||||
import { Input, Button } from '@grafana/ui'; |
||||
import { appEvents } from 'app/core/core'; |
||||
|
||||
interface Props { |
||||
scope: LiveChannelScope; |
||||
namespace: string; |
||||
path: string; |
||||
config?: LiveChannelConfig; |
||||
} |
||||
|
||||
interface State { |
||||
channel?: LiveChannel; |
||||
status: LiveChannelStatusEvent; |
||||
count: number; |
||||
lastTime: number; |
||||
lastBody: string; |
||||
text: string; // for publish!
|
||||
} |
||||
|
||||
export class LivePanel extends PureComponent<Props, State> { |
||||
state: State = { |
||||
status: { |
||||
type: LiveChannelEventType.Status, |
||||
id: '?', |
||||
state: LiveChannelConnectionState.Pending, |
||||
timestamp: Date.now(), |
||||
}, |
||||
count: 0, |
||||
lastTime: 0, |
||||
lastBody: '', |
||||
text: '', |
||||
}; |
||||
subscription?: Unsubscribable; |
||||
|
||||
streamObserver: PartialObserver<LiveChannelEvent> = { |
||||
next: (event: LiveChannelEvent) => { |
||||
if (isLiveChannelStatusEvent(event)) { |
||||
this.setState({ status: event }); |
||||
} else { |
||||
this.setState({ |
||||
count: this.state.count + 1, |
||||
lastTime: Date.now(), |
||||
lastBody: JSON.stringify(event), |
||||
}); |
||||
} |
||||
}, |
||||
}; |
||||
|
||||
startSubscription = () => { |
||||
const { scope, namespace, path } = this.props; |
||||
const channel = getGrafanaLiveSrv().getChannel({ scope, namespace, path }); |
||||
if (this.state.channel === channel) { |
||||
return; // no change!
|
||||
} |
||||
|
||||
if (this.subscription) { |
||||
this.subscription.unsubscribe(); |
||||
} |
||||
|
||||
this.subscription = channel.getStream().subscribe(this.streamObserver); |
||||
this.setState({ channel }); |
||||
}; |
||||
|
||||
componentDidMount = () => { |
||||
this.startSubscription(); |
||||
}; |
||||
|
||||
componentWillUnmount() { |
||||
if (this.subscription) { |
||||
this.subscription.unsubscribe(); |
||||
} |
||||
} |
||||
|
||||
componentDidUpdate(oldProps: Props) { |
||||
if (oldProps.config !== this.props.config) { |
||||
this.startSubscription(); |
||||
} |
||||
} |
||||
|
||||
onTextChanged = (event: React.ChangeEvent<HTMLInputElement>) => { |
||||
this.setState({ text: event.target.value }); |
||||
}; |
||||
|
||||
onPublish = () => { |
||||
const { text, channel } = this.state; |
||||
if (text && channel) { |
||||
const msg = { |
||||
line: text, |
||||
}; |
||||
|
||||
channel.publish!(msg) |
||||
.then(v => { |
||||
console.log('PUBLISHED', text, v); |
||||
}) |
||||
.catch(err => { |
||||
appEvents.emit(AppEvents.alertError, ['Publish error', `${err}`]); |
||||
}); |
||||
} |
||||
this.setState({ text: '' }); |
||||
}; |
||||
|
||||
render() { |
||||
const { lastBody, lastTime, count, status, text } = this.state; |
||||
const { config } = this.props; |
||||
const showPublish = config && config.canPublish && config.canPublish(); |
||||
|
||||
return ( |
||||
<div> |
||||
<h5>Status: {config ? '' : '(no config)'}</h5> |
||||
<pre>{JSON.stringify(status)}</pre> |
||||
|
||||
<h5>Count: {count}</h5> |
||||
{lastTime > 0 && ( |
||||
<> |
||||
<h5>Last: {lastTime}</h5> |
||||
{lastBody && ( |
||||
<div> |
||||
<pre>{lastBody}</pre> |
||||
</div> |
||||
)} |
||||
</> |
||||
)} |
||||
|
||||
{showPublish && ( |
||||
<div> |
||||
<h3>Write to channel</h3> |
||||
<Input value={text} onChange={this.onTextChanged} /> |
||||
<Button onClick={this.onPublish} variant={text ? 'primary' : 'secondary'}> |
||||
Publish |
||||
</Button> |
||||
</div> |
||||
)} |
||||
</div> |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,155 @@ |
||||
import React, { PureComponent } from 'react'; |
||||
import { css } from 'emotion'; |
||||
import { Select, FeatureInfoBox, Label, stylesFactory } from '@grafana/ui'; |
||||
import { |
||||
LiveChannelScope, |
||||
LiveChannelAddress, |
||||
LiveChannelSupport, |
||||
LiveChannelConfig, |
||||
SelectableValue, |
||||
StandardEditorProps, |
||||
FeatureState, |
||||
GrafanaTheme, |
||||
} from '@grafana/data'; |
||||
|
||||
import { LivePanelOptions } from './types'; |
||||
import { getGrafanaLiveCentrifugeSrv } from 'app/features/live/live'; |
||||
import { config } from 'app/core/config'; |
||||
|
||||
type Props = StandardEditorProps<LiveChannelAddress, any, LivePanelOptions>; |
||||
|
||||
const scopes: Array<SelectableValue<LiveChannelScope>> = [ |
||||
{ label: 'Grafana', value: LiveChannelScope.Grafana, description: 'Core grafana live features' }, |
||||
{ label: 'Data Sources', value: LiveChannelScope.DataSource, description: 'Data sources with live support' }, |
||||
{ label: 'Plugins', value: LiveChannelScope.Plugin, description: 'Plugins with live support' }, |
||||
]; |
||||
|
||||
interface State { |
||||
namespaces: Array<SelectableValue<string>>; |
||||
paths: Array<SelectableValue<string>>; |
||||
support?: LiveChannelSupport; |
||||
config?: LiveChannelConfig; |
||||
} |
||||
|
||||
export class LiveChannelEditor extends PureComponent<Props, State> { |
||||
state: State = { |
||||
namespaces: [], |
||||
paths: [], |
||||
}; |
||||
|
||||
async componentDidMount() { |
||||
this.updateSelectOptions(); |
||||
} |
||||
|
||||
async componentDidUpdate(oldProps: Props) { |
||||
if (this.props.value !== oldProps.value) { |
||||
this.updateSelectOptions(); |
||||
} |
||||
} |
||||
|
||||
async updateSelectOptions() { |
||||
const { scope, namespace, path } = this.props.value; |
||||
const srv = getGrafanaLiveCentrifugeSrv(); |
||||
const namespaces = await srv.scopes[scope].listNamespaces(); |
||||
const support = namespace ? await srv.scopes[scope].getChannelSupport(namespace) : undefined; |
||||
const paths = support ? await support.getSupportedPaths() : undefined; |
||||
const config = support && path ? await support.getChannelConfig(path) : undefined; |
||||
|
||||
this.setState({ |
||||
namespaces, |
||||
support, |
||||
paths: paths |
||||
? paths.map(p => ({ |
||||
label: p.path, |
||||
value: p.path, |
||||
description: p.description, |
||||
})) |
||||
: [], |
||||
config, |
||||
}); |
||||
} |
||||
|
||||
onScopeChanged = (v: SelectableValue<LiveChannelScope>) => { |
||||
if (v.value) { |
||||
this.props.onChange({ scope: v.value } as LiveChannelAddress); |
||||
} |
||||
}; |
||||
|
||||
onNamespaceChanged = (v: SelectableValue<string>) => { |
||||
const update = { |
||||
scope: this.props.value?.scope, |
||||
} as LiveChannelAddress; |
||||
if (v.value) { |
||||
update.namespace = v.value; |
||||
} |
||||
this.props.onChange(update); |
||||
}; |
||||
|
||||
onPathChanged = (v: SelectableValue<string>) => { |
||||
const { value, onChange } = this.props; |
||||
const update = { |
||||
scope: value.scope, |
||||
namespace: value.namespace, |
||||
} as LiveChannelAddress; |
||||
if (v.value) { |
||||
update.path = v.value; |
||||
} |
||||
onChange(update); |
||||
}; |
||||
|
||||
render() { |
||||
const { namespaces, paths } = this.state; |
||||
const { scope, namespace, path } = this.props.value; |
||||
const style = getStyles(config.theme); |
||||
|
||||
return ( |
||||
<> |
||||
<FeatureInfoBox title="Grafana Live" featureState={FeatureState.alpha}> |
||||
<p> |
||||
This supports real-time event streams in grafana core. This feature is under heavy development. Expect the |
||||
intefaces and structures to change as this becomes more production ready. |
||||
</p> |
||||
</FeatureInfoBox> |
||||
|
||||
<div> |
||||
<div className={style.dropWrap}> |
||||
<Label>Scope</Label> |
||||
<Select options={scopes} value={scopes.find(s => s.value === scope)} onChange={this.onScopeChanged} /> |
||||
</div> |
||||
|
||||
{scope && ( |
||||
<div className={style.dropWrap}> |
||||
<Label>Namespace</Label> |
||||
<Select |
||||
options={namespaces} |
||||
value={namespaces.find(s => s.value === namespace) || namespace || ''} |
||||
onChange={this.onNamespaceChanged} |
||||
allowCustomValue={true} |
||||
backspaceRemovesValue={true} |
||||
/> |
||||
</div> |
||||
)} |
||||
|
||||
{scope && namespace && ( |
||||
<div className={style.dropWrap}> |
||||
<Label>Path</Label> |
||||
<Select |
||||
options={paths} |
||||
value={paths.find(s => s.value === path) || path || ''} |
||||
onChange={this.onPathChanged} |
||||
allowCustomValue={true} |
||||
backspaceRemovesValue={true} |
||||
/> |
||||
</div> |
||||
)} |
||||
</div> |
||||
</> |
||||
); |
||||
} |
||||
} |
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => ({ |
||||
dropWrap: css` |
||||
margin-bottom: ${theme.spacing.sm}; |
||||
`,
|
||||
})); |
@ -0,0 +1,156 @@ |
||||
import React, { PureComponent } from 'react'; |
||||
import { Unsubscribable, PartialObserver } from 'rxjs'; |
||||
import { CustomScrollbar, FeatureInfoBox, Label } from '@grafana/ui'; |
||||
import { |
||||
PanelProps, |
||||
LiveChannelStatusEvent, |
||||
isValidLiveChannelAddress, |
||||
LiveChannel, |
||||
LiveChannelEvent, |
||||
isLiveChannelStatusEvent, |
||||
isLiveChannelMessageEvent, |
||||
} from '@grafana/data'; |
||||
import { LivePanelOptions } from './types'; |
||||
import { getGrafanaLiveSrv } from '@grafana/runtime'; |
||||
|
||||
interface Props extends PanelProps<LivePanelOptions> {} |
||||
|
||||
interface State { |
||||
error?: any; |
||||
channel?: LiveChannel; |
||||
status?: LiveChannelStatusEvent; |
||||
message?: any; |
||||
} |
||||
|
||||
export class LivePanel extends PureComponent<Props, State> { |
||||
private readonly isValid: boolean; |
||||
subscription?: Unsubscribable; |
||||
|
||||
constructor(props: Props) { |
||||
super(props); |
||||
|
||||
this.isValid = !!getGrafanaLiveSrv(); |
||||
this.state = {}; |
||||
} |
||||
|
||||
async componentDidMount() { |
||||
this.loadChannel(); |
||||
} |
||||
|
||||
componentWillUnmount() { |
||||
if (this.subscription) { |
||||
this.subscription.unsubscribe(); |
||||
} |
||||
} |
||||
|
||||
componentDidUpdate(prevProps: Props): void { |
||||
if (this.props.options?.channel !== prevProps.options?.channel) { |
||||
this.loadChannel(); |
||||
} |
||||
} |
||||
|
||||
streamObserver: PartialObserver<LiveChannelEvent> = { |
||||
next: (event: LiveChannelEvent) => { |
||||
if (isLiveChannelStatusEvent(event)) { |
||||
this.setState({ status: event }); |
||||
} else if (isLiveChannelMessageEvent(event)) { |
||||
this.setState({ message: event.message }); |
||||
} else { |
||||
console.log('ignore', event); |
||||
} |
||||
}, |
||||
}; |
||||
|
||||
unsubscribe = () => { |
||||
if (this.subscription) { |
||||
this.subscription.unsubscribe(); |
||||
this.subscription = undefined; |
||||
} |
||||
}; |
||||
|
||||
async loadChannel() { |
||||
const addr = this.props.options?.channel; |
||||
if (!isValidLiveChannelAddress(addr)) { |
||||
console.log('INVALID', addr); |
||||
this.unsubscribe(); |
||||
this.setState({ |
||||
channel: undefined, |
||||
}); |
||||
return; |
||||
} |
||||
|
||||
const channel = getGrafanaLiveSrv().getChannel(addr); |
||||
const changed = channel.id !== this.state.channel?.id; |
||||
console.log('LOAD', addr, changed, channel); |
||||
if (changed) { |
||||
this.unsubscribe(); |
||||
|
||||
// Subscribe to new events
|
||||
try { |
||||
this.subscription = channel.getStream().subscribe(this.streamObserver); |
||||
this.setState({ channel, error: undefined }); |
||||
} catch (err) { |
||||
this.setState({ channel: undefined, error: err }); |
||||
} |
||||
} else { |
||||
console.log('Same channel', channel); |
||||
} |
||||
} |
||||
|
||||
renderNotEnabled() { |
||||
const preformatted = `[feature_toggles]
|
||||
enable = live`;
|
||||
return ( |
||||
<FeatureInfoBox |
||||
title="Grafana Live" |
||||
style={{ |
||||
height: this.props.height, |
||||
}} |
||||
> |
||||
<p>Grafana live requires a feature flag to run</p> |
||||
|
||||
<b>custom.ini:</b> |
||||
<pre>{preformatted}</pre> |
||||
</FeatureInfoBox> |
||||
); |
||||
} |
||||
|
||||
render() { |
||||
if (!this.isValid) { |
||||
return this.renderNotEnabled(); |
||||
} |
||||
const { channel, status, message, error } = this.state; |
||||
if (!channel) { |
||||
return ( |
||||
<FeatureInfoBox |
||||
title="Grafana Live" |
||||
style={{ |
||||
height: this.props.height, |
||||
}} |
||||
> |
||||
<p>Use the panel editor to pick a channel</p> |
||||
</FeatureInfoBox> |
||||
); |
||||
} |
||||
|
||||
if (error) { |
||||
return ( |
||||
<div> |
||||
<h2>ERROR</h2> |
||||
<div>{JSON.stringify(error)}</div> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
return ( |
||||
<CustomScrollbar autoHeightMin="100%" autoHeightMax="100%"> |
||||
<Label>Status</Label> |
||||
<pre>{JSON.stringify(status)}</pre> |
||||
|
||||
<br /> |
||||
<Label>Message</Label> |
||||
<pre>{JSON.stringify(message)}</pre> |
||||
</CustomScrollbar> |
||||
); |
||||
} |
||||
} |
After Width: | Height: | Size: 1.4 KiB |
@ -0,0 +1,14 @@ |
||||
import { PanelPlugin } from '@grafana/data'; |
||||
import { LiveChannelEditor } from './LiveChannelEditor'; |
||||
import { LivePanel } from './LivePanel'; |
||||
import { LivePanelOptions } from './types'; |
||||
|
||||
export const plugin = new PanelPlugin<LivePanelOptions>(LivePanel).setPanelOptions(builder => { |
||||
builder.addCustomEditor({ |
||||
id: 'channel', |
||||
path: 'channel', |
||||
name: 'Channel', |
||||
editor: LiveChannelEditor, |
||||
defaultValue: {}, |
||||
}); |
||||
}); |
@ -0,0 +1,20 @@ |
||||
{ |
||||
"type": "panel", |
||||
"name": "Live", |
||||
"id": "live", |
||||
|
||||
"skipDataQuery": true, |
||||
|
||||
"state": "alpha", |
||||
|
||||
"info": { |
||||
"author": { |
||||
"name": "Grafana Labs", |
||||
"url": "https://grafana.com" |
||||
}, |
||||
"logos": { |
||||
"small": "img/live.svg", |
||||
"large": "img/live.svg" |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,5 @@ |
||||
import { LiveChannelAddress } from '@grafana/data'; |
||||
|
||||
export interface LivePanelOptions { |
||||
channel?: LiveChannelAddress; |
||||
} |
Loading…
Reference in new issue