mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Canvas: add alpha canvas panel and initial interfaces (#37279)
This commit is contained in:
97
public/app/plugins/panel/canvas/CanvasPanel.tsx
Normal file
97
public/app/plugins/panel/canvas/CanvasPanel.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { Component } from 'react';
|
||||
import { PanelProps } from '@grafana/data';
|
||||
import { PanelOptions } from './models.gen';
|
||||
import { ReplaySubject, Subscription } from 'rxjs';
|
||||
import { PanelEditExitedEvent } from 'app/types/events';
|
||||
import { CanvasGroupOptions } from 'app/features/canvas';
|
||||
import { Scene } from 'app/features/canvas/runtime/scene';
|
||||
|
||||
interface Props extends PanelProps<PanelOptions> {}
|
||||
|
||||
interface State {
|
||||
refresh: number;
|
||||
}
|
||||
|
||||
// Used to pass the scene to the editor functions
|
||||
export const theScene = new ReplaySubject<Scene>(1);
|
||||
|
||||
export class CanvasPanel extends Component<Props, State> {
|
||||
readonly scene: Scene;
|
||||
private subs = new Subscription();
|
||||
needsReload = false;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
refresh: 0,
|
||||
};
|
||||
|
||||
// Only the initial options are ever used.
|
||||
// later changs are all controled by the scene
|
||||
this.scene = new Scene(this.props.options.root, this.onUpdateScene);
|
||||
this.scene.updateSize(props.width, props.height);
|
||||
this.scene.updateData(props.data);
|
||||
theScene.next(this.scene); // used in the editors
|
||||
|
||||
this.subs.add(
|
||||
this.props.eventBus.subscribe(PanelEditExitedEvent, (evt) => {
|
||||
if (this.props.id === evt.payload) {
|
||||
this.needsReload = true;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
theScene.next(this.scene);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.subs.unsubscribe();
|
||||
}
|
||||
|
||||
// NOTE, all changes to the scene flow through this function
|
||||
// even the editor gets current state from the same scene instance!
|
||||
onUpdateScene = (root: CanvasGroupOptions) => {
|
||||
const { onOptionsChange, options } = this.props;
|
||||
onOptionsChange({
|
||||
...options,
|
||||
root,
|
||||
});
|
||||
this.setState({ refresh: this.state.refresh + 1 });
|
||||
// console.log('send changes', root);
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps: Props) {
|
||||
const { width, height, data, renderCounter } = this.props;
|
||||
let changed = false;
|
||||
|
||||
if (width !== nextProps.width || height !== nextProps.height) {
|
||||
this.scene.updateSize(nextProps.width, nextProps.height);
|
||||
changed = true;
|
||||
}
|
||||
if (data !== nextProps.data) {
|
||||
this.scene.updateData(nextProps.data);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// After editing, the options are valid, but the scene was in a different panel
|
||||
if (this.needsReload && this.props.options !== nextProps.options) {
|
||||
this.needsReload = false;
|
||||
this.scene.load(nextProps.options.root);
|
||||
this.scene.updateSize(nextProps.width, nextProps.height);
|
||||
this.scene.updateData(nextProps.data);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (renderCounter !== nextProps.renderCounter) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.scene.render();
|
||||
}
|
||||
}
|
||||
4
public/app/plugins/panel/canvas/README.md
Normal file
4
public/app/plugins/panel/canvas/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# Canvas panel - Native Plugin
|
||||
|
||||
The Canvas Panel is **included** with Grafana.
|
||||
|
||||
123
public/app/plugins/panel/canvas/editor/ElementEditor.tsx
Normal file
123
public/app/plugins/panel/canvas/editor/ElementEditor.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import React, { FC, useMemo } from 'react';
|
||||
import { Select } from '@grafana/ui';
|
||||
import { DataFrame, PanelOptionsEditorBuilder, StandardEditorContext } from '@grafana/data';
|
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
import { setOptionImmutably } from 'app/features/dashboard/components/PanelEditor/utils';
|
||||
import { fillOptionsPaneItems } from 'app/features/dashboard/components/PanelEditor/getVizualizationOptions';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { addBackgroundOptions, addBorderOptions } from './options';
|
||||
import {
|
||||
CanvasElementItem,
|
||||
CanvasElementOptions,
|
||||
canvasElementRegistry,
|
||||
DEFAULT_CANVAS_ELEMENT_CONFIG,
|
||||
} from 'app/features/canvas';
|
||||
|
||||
export interface CanvasElementEditorProps<TConfig = any> {
|
||||
options?: CanvasElementOptions<TConfig>;
|
||||
data: DataFrame[]; // All results
|
||||
onChange: (options: CanvasElementOptions<TConfig>) => void;
|
||||
filter?: (item: CanvasElementItem) => boolean;
|
||||
}
|
||||
|
||||
export const CanvasElementEditor: FC<CanvasElementEditorProps> = ({ options, onChange, data, filter }) => {
|
||||
// all basemaps
|
||||
const layerTypes = useMemo(() => {
|
||||
return canvasElementRegistry.selectOptions(
|
||||
options?.type // the selected value
|
||||
? [options.type] // as an array
|
||||
: [DEFAULT_CANVAS_ELEMENT_CONFIG.type],
|
||||
filter
|
||||
);
|
||||
}, [options?.type, filter]);
|
||||
|
||||
// The options change with each layer type
|
||||
const optionsEditorBuilder = useMemo(() => {
|
||||
const layer = canvasElementRegistry.getIfExists(options?.type);
|
||||
if (!layer || !layer.registerOptionsUI) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const builder = new PanelOptionsEditorBuilder<CanvasElementOptions>();
|
||||
if (layer.registerOptionsUI) {
|
||||
layer.registerOptionsUI(builder);
|
||||
}
|
||||
|
||||
addBackgroundOptions(builder);
|
||||
addBorderOptions(builder);
|
||||
return builder;
|
||||
}, [options?.type]);
|
||||
|
||||
// The react componnets
|
||||
const layerOptions = useMemo(() => {
|
||||
const layer = canvasElementRegistry.getIfExists(options?.type);
|
||||
if (!optionsEditorBuilder || !layer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const category = new OptionsPaneCategoryDescriptor({
|
||||
id: 'CanvasElement config',
|
||||
title: 'CanvasElement config',
|
||||
});
|
||||
|
||||
const context: StandardEditorContext<any> = {
|
||||
data,
|
||||
options: options,
|
||||
};
|
||||
|
||||
const currentOptions = { ...options, type: layer.id, config: { ...layer.defaultConfig, ...options?.config } };
|
||||
|
||||
// Update the panel options if not set
|
||||
if (!options || (layer.defaultConfig && !options.config)) {
|
||||
onChange(currentOptions as any);
|
||||
}
|
||||
|
||||
const reg = optionsEditorBuilder.getRegistry();
|
||||
|
||||
// Load the options into categories
|
||||
fillOptionsPaneItems(
|
||||
reg.list(),
|
||||
|
||||
// Always use the same category
|
||||
(categoryNames) => category,
|
||||
|
||||
// Custom upate function
|
||||
(path: string, value: any) => {
|
||||
onChange(setOptionImmutably(currentOptions, path, value) as any);
|
||||
},
|
||||
context
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<br />
|
||||
{category.items.map((item) => item.render())}
|
||||
</>
|
||||
);
|
||||
}, [optionsEditorBuilder, onChange, data, options]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Select
|
||||
menuShouldPortal
|
||||
options={layerTypes.options}
|
||||
value={layerTypes.current}
|
||||
onChange={(v) => {
|
||||
const layer = canvasElementRegistry.getIfExists(v.value);
|
||||
if (!layer) {
|
||||
console.warn('layer does not exist', v);
|
||||
return;
|
||||
}
|
||||
|
||||
onChange({
|
||||
...options, // keep current options
|
||||
type: layer.id,
|
||||
config: cloneDeep(layer.defaultConfig ?? {}),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
{layerOptions}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import React, { FC } from 'react';
|
||||
import { StandardEditorProps } from '@grafana/data';
|
||||
import { PanelOptions } from '../models.gen';
|
||||
import { CanvasElementEditor } from './ElementEditor';
|
||||
import { theScene } from '../CanvasPanel';
|
||||
import { useObservable } from 'react-use';
|
||||
import { of } from 'rxjs';
|
||||
import { CanvasGroupOptions } from 'app/features/canvas';
|
||||
|
||||
export const SelectedElementEditor: FC<StandardEditorProps<CanvasGroupOptions, any, PanelOptions>> = ({ context }) => {
|
||||
const scene = useObservable(theScene);
|
||||
const selected = useObservable(scene?.selected ?? of(undefined));
|
||||
|
||||
if (!selected) {
|
||||
return <div>No item is selected</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<CanvasElementEditor
|
||||
options={selected.options}
|
||||
data={context.data}
|
||||
onChange={(cfg) => {
|
||||
scene!.onChange(selected.UID, cfg);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
66
public/app/plugins/panel/canvas/editor/options.ts
Normal file
66
public/app/plugins/panel/canvas/editor/options.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { PanelOptionsEditorBuilder } from '@grafana/data';
|
||||
import { BackgroundImageSize } from 'app/features/canvas';
|
||||
import { ColorDimensionEditor, ResourceDimensionEditor } from 'app/features/dimensions/editors';
|
||||
|
||||
export function addBackgroundOptions(builder: PanelOptionsEditorBuilder<any>) {
|
||||
builder
|
||||
.addCustomEditor({
|
||||
id: 'background.color',
|
||||
path: 'background.color',
|
||||
name: 'Background Color',
|
||||
editor: ColorDimensionEditor,
|
||||
settings: {},
|
||||
defaultValue: {
|
||||
// Configured values
|
||||
fixed: '',
|
||||
},
|
||||
})
|
||||
.addCustomEditor({
|
||||
id: 'background.image',
|
||||
path: 'background.image',
|
||||
name: 'Background Image',
|
||||
editor: ResourceDimensionEditor,
|
||||
settings: {
|
||||
resourceType: 'image',
|
||||
},
|
||||
})
|
||||
.addRadio({
|
||||
path: 'background.size',
|
||||
name: 'Backround image size',
|
||||
settings: {
|
||||
options: [
|
||||
{ value: BackgroundImageSize.Original, label: 'Original' },
|
||||
{ value: BackgroundImageSize.Contain, label: 'Contain' },
|
||||
{ value: BackgroundImageSize.Cover, label: 'Cover' },
|
||||
{ value: BackgroundImageSize.Fill, label: 'Fill' },
|
||||
{ value: BackgroundImageSize.Tile, label: 'Tile' },
|
||||
],
|
||||
},
|
||||
defaultValue: BackgroundImageSize.Cover,
|
||||
});
|
||||
}
|
||||
|
||||
export function addBorderOptions(builder: PanelOptionsEditorBuilder<any>) {
|
||||
builder.addSliderInput({
|
||||
path: 'border.width',
|
||||
name: 'Border Width',
|
||||
defaultValue: 2,
|
||||
settings: {
|
||||
min: 0,
|
||||
max: 20,
|
||||
},
|
||||
});
|
||||
|
||||
builder.addCustomEditor({
|
||||
id: 'border.color',
|
||||
path: 'border.color',
|
||||
name: 'Border Color',
|
||||
editor: ColorDimensionEditor,
|
||||
settings: {},
|
||||
defaultValue: {
|
||||
// Configured values
|
||||
fixed: '',
|
||||
},
|
||||
showIf: (cfg) => Boolean(cfg.border?.width),
|
||||
});
|
||||
}
|
||||
9
public/app/plugins/panel/canvas/img/icn-canvas.svg
Normal file
9
public/app/plugins/panel/canvas/img/icn-canvas.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve">
|
||||
<defs><style>.cls-1{fill:#84aff1;}.cls-2{fill:url(#linear-gradient);}.cls-3{fill:#3865ab;}</style><linearGradient id="linear-gradient" y1="40.18" x2="82.99" y2="40.18" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f2cc0c"/><stop offset="1" stop-color="#ff9830"/></linearGradient></defs>
|
||||
<g>
|
||||
<path class="cls-3" d="M869,351v543.4H97.6V123.1h596V17.9H81.8C34.9,17.9,10,50.4,10,97.3v814.7c0,46.9,45.8,70.1,79.9,70.1h806.4c44.4,0,60.3-23.3,60.3-70.1V351H869z"/>
|
||||
<path class="cls-2" d="M888.2,263.3L765.4,140.6L302.6,606l120,120L888.2,263.3z"/>
|
||||
<path class="cls-1" d="M204.5,824.3l192.8-70.1L274.6,631.5L204.5,824.3z"/>
|
||||
<path class="cls-1" d="M977.6,111.1l-60-60c-16.6-16.6-43.4-16.6-60,0l-60,60l120,120l60-60C994.1,154.5,994.1,127.6,977.6,111.1z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1004 B |
29
public/app/plugins/panel/canvas/models.cue
Normal file
29
public/app/plugins/panel/canvas/models.cue
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright 2021 Grafana Labs
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package grafanaschema
|
||||
|
||||
Family: {
|
||||
lineages: [
|
||||
[
|
||||
{
|
||||
PanelOptions: {
|
||||
// anything for now
|
||||
...
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
migrations: []
|
||||
}
|
||||
22
public/app/plugins/panel/canvas/models.gen.ts
Normal file
22
public/app/plugins/panel/canvas/models.gen.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// NOTE: This file will be auto generated from models.cue
|
||||
// It is currenty hand written but will serve as the target for cuetsy
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
import { CanvasGroupOptions, DEFAULT_CANVAS_ELEMENT_CONFIG } from 'app/features/canvas';
|
||||
|
||||
export const modelVersion = Object.freeze([1, 0]);
|
||||
|
||||
export interface PanelOptions {
|
||||
root: CanvasGroupOptions;
|
||||
}
|
||||
|
||||
export const defaultPanelOptions: PanelOptions = {
|
||||
root: ({
|
||||
elements: [
|
||||
{
|
||||
...DEFAULT_CANVAS_ELEMENT_CONFIG,
|
||||
},
|
||||
],
|
||||
} as unknown) as CanvasGroupOptions,
|
||||
};
|
||||
19
public/app/plugins/panel/canvas/module.tsx
Normal file
19
public/app/plugins/panel/canvas/module.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { PanelPlugin } from '@grafana/data';
|
||||
|
||||
import { CanvasPanel } from './CanvasPanel';
|
||||
import { SelectedElementEditor } from './editor/SelectedElementEditor';
|
||||
import { defaultPanelOptions, PanelOptions } from './models.gen';
|
||||
|
||||
export const plugin = new PanelPlugin<PanelOptions>(CanvasPanel)
|
||||
.setNoPadding() // extend to panel edges
|
||||
.useFieldConfig()
|
||||
.setPanelOptions((builder) => {
|
||||
builder.addCustomEditor({
|
||||
category: ['Selected Element'],
|
||||
id: 'root',
|
||||
path: 'root', // multiple elements may edit root!
|
||||
name: 'Selected Element',
|
||||
editor: SelectedElementEditor,
|
||||
defaultValue: defaultPanelOptions.root,
|
||||
});
|
||||
});
|
||||
18
public/app/plugins/panel/canvas/plugin.json
Normal file
18
public/app/plugins/panel/canvas/plugin.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "panel",
|
||||
"name": "Canvas",
|
||||
"id": "canvas",
|
||||
"state": "alpha",
|
||||
|
||||
"info": {
|
||||
"description": "Explict element placement",
|
||||
"author": {
|
||||
"name": "Grafana Labs",
|
||||
"url": "https://grafana.com"
|
||||
},
|
||||
"logos": {
|
||||
"small": "img/icn-canvas.svg",
|
||||
"large": "img/icn-canvas.svg"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user