Initial Version
This commit is contained in:
+12
@@ -0,0 +1,12 @@
|
||||
CHANGELOG.md
|
||||
dist
|
||||
docker-compose.yaml
|
||||
eslint.config.mjs
|
||||
jest.config.js
|
||||
jest-setup.js
|
||||
node_modules
|
||||
package-lock.json
|
||||
playwright.config.ts
|
||||
provisioning
|
||||
tests
|
||||
tsconfig.json
|
||||
@@ -0,0 +1,11 @@
|
||||
# Grafana Battery Visualisation
|
||||
|
||||
I hate writing documentation, and since I will have ADHD brain, I will forget how to do this.
|
||||
|
||||
```
|
||||
npm install
|
||||
npm run build
|
||||
cp -r dist /var/lib/grafana/plugins/andydixon-battery-panel
|
||||
```
|
||||
|
||||
in `/etc/grafana/grsfana.ini` add `andydixon-battery-panel` to `allow_loading_unsigned_plugins`
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"name": "battery",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"build": "webpack -c ./.config/webpack/webpack.config.ts --env production",
|
||||
"dev": "webpack -w -c ./.config/webpack/webpack.config.ts --env development",
|
||||
"test": "jest --watch --onlyChanged",
|
||||
"test:ci": "jest --passWithNoTests --maxWorkers 4",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "eslint --cache .",
|
||||
"lint:fix": "npm run lint -- --fix && prettier --write --list-different .",
|
||||
"e2e": "playwright test",
|
||||
"server": "docker compose up --build",
|
||||
"sign": "npx --yes @grafana/sign-plugin@latest"
|
||||
},
|
||||
"author": "Andy dixon",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@grafana/eslint-config": "^8.2.0",
|
||||
"@grafana/plugin-e2e": "^2.2.0",
|
||||
"@grafana/tsconfig": "^2.0.0",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@stylistic/eslint-plugin-ts": "^2.9.0",
|
||||
"@swc/core": "^1.3.90",
|
||||
"@swc/helpers": "^0.5.0",
|
||||
"@swc/jest": "^0.2.26",
|
||||
"@testing-library/jest-dom": "6.1.4",
|
||||
"@testing-library/react": "14.0.0",
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/node": "^20.8.7",
|
||||
"@types/testing-library__jest-dom": "5.14.8",
|
||||
"@typescript-eslint/eslint-plugin": "^8.3.0",
|
||||
"@typescript-eslint/parser": "^8.3.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"css-loader": "^6.7.3",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-jsdoc": "^51.2.3",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-webpack-plugin": "^5.0.0",
|
||||
"fork-ts-checker-webpack-plugin": "^8.0.0",
|
||||
"glob": "^10.2.7",
|
||||
"identity-obj-proxy": "3.0.0",
|
||||
"imports-loader": "^5.0.0",
|
||||
"jest": "^29.5.0",
|
||||
"jest-environment-jsdom": "^29.5.0",
|
||||
"prettier": "^2.8.7",
|
||||
"replace-in-file-webpack-plugin": "^1.0.6",
|
||||
"sass": "1.63.2",
|
||||
"sass-loader": "13.3.1",
|
||||
"semver": "^7.6.3",
|
||||
"style-loader": "3.3.3",
|
||||
"swc-loader": "^0.2.3",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "5.5.4",
|
||||
"webpack": "^5.94.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-livereload-plugin": "^3.0.2",
|
||||
"webpack-subresource-integrity": "^5.1.0",
|
||||
"webpack-virtual-modules": "^0.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/css": "11.10.6",
|
||||
"@grafana/data": "^12.2.0",
|
||||
"@grafana/i18n": "^12.2.0",
|
||||
"@grafana/runtime": "^12.2.0",
|
||||
"@grafana/ui": "^12.2.0",
|
||||
"@grafana/schema": "^12.2.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"packageManager": "npm@10.8.2"
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<!-- This README file is going to be the one displayed on the Grafana.com website for your plugin. Uncomment and replace the content here before publishing.
|
||||
|
||||
Remove any remaining comments before publishing as these may be displayed on Grafana.com -->
|
||||
|
||||
# Battery
|
||||
|
||||
<!-- To help maximize the impact of your README and improve usability for users, we propose the following loose structure:
|
||||
|
||||
**BEFORE YOU BEGIN**
|
||||
- Ensure all links are absolute URLs so that they will work when the README is displayed within Grafana and Grafana.com
|
||||
- Be inspired ✨
|
||||
- [grafana-polystat-panel](https://github.com/grafana/grafana-polystat-panel)
|
||||
- [volkovlabs-variable-panel](https://github.com/volkovlabs/volkovlabs-variable-panel)
|
||||
|
||||
**ADD SOME BADGES**
|
||||
|
||||
Badges convey useful information at a glance for users whether in the Catalog or viewing the source code. You can use the generator on [Shields.io](https://shields.io/badges/dynamic-json-badge) together with the Grafana.com API
|
||||
to create dynamic badges that update automatically when you publish a new version to the marketplace.
|
||||
|
||||
- For the URL parameter use `https://grafana.com/api/plugins/your-plugin-id`.
|
||||
- Example queries:
|
||||
- Downloads: `$.downloads`
|
||||
- Catalog Version: `$.version`
|
||||
- Grafana Dependency: `$.grafanaDependency`
|
||||
- Signature Type: `$.versionSignatureType`
|
||||
- Optionally, for the logo parameter use `grafana`.
|
||||
|
||||
Full example: 
|
||||
|
||||
Consider other [badges](https://shields.io/badges) as you feel appropriate for your project.
|
||||
|
||||
## Overview / Introduction
|
||||
Provide one or more paragraphs as an introduction to your plugin to help users understand why they should use it.
|
||||
|
||||
Consider including screenshots:
|
||||
- in [plugin.json](https://grafana.com/developers/plugin-tools/reference/plugin-json#info) include them as relative links.
|
||||
- in the README ensure they are absolute URLs.
|
||||
|
||||
## Requirements
|
||||
List any requirements or dependencies they may need to run the plugin.
|
||||
|
||||
## Getting Started
|
||||
Provide a quick start on how to configure and use the plugin.
|
||||
|
||||
## Documentation
|
||||
If your project has dedicated documentation available for users, provide links here. For help in following Grafana's style recommendations for technical documentation, refer to our [Writer's Toolkit](https://grafana.com/docs/writers-toolkit/).
|
||||
|
||||
## Contributing
|
||||
Do you want folks to contribute to the plugin or provide feedback through specific means? If so, tell them how!
|
||||
-->
|
||||
@@ -0,0 +1,254 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// ChargeVizPanel.tsx
|
||||
// Responsive Grafana battery visual with fine-tuned spacing and optional text hiding.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
import React from 'react';
|
||||
import { PanelProps } from '@grafana/data';
|
||||
import { useTheme2 } from '@grafana/ui';
|
||||
|
||||
interface ChargeVizOptions {
|
||||
lowColour: string;
|
||||
midColour: string;
|
||||
highColour: string;
|
||||
gradientFill: boolean;
|
||||
decimalPlaces: number;
|
||||
useRegressionForRate: boolean;
|
||||
hideText: boolean;
|
||||
}
|
||||
|
||||
interface Props extends PanelProps<ChargeVizOptions> {}
|
||||
|
||||
const toFinite = (v: any): number | undefined => {
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : undefined;
|
||||
};
|
||||
|
||||
// Computes a least-squares slope (% per minute) to smooth noisy data.
|
||||
function slopePercentPerMinute(times: number[], values: number[]): number {
|
||||
const pts: Array<[number, number]> = [];
|
||||
for (let i = 0; i < times.length; i++) {
|
||||
const t = toFinite(times[i]);
|
||||
const v = toFinite(values[i]);
|
||||
if (t !== undefined && v !== undefined) pts.push([t, v]);
|
||||
}
|
||||
if (pts.length < 2) return 0;
|
||||
|
||||
const xs = pts.map(p => (p[0] - pts[0][0]) / 60000);
|
||||
const ys = pts.map(p => p[1]);
|
||||
const xMean = xs.reduce((a, b) => a + b, 0) / xs.length;
|
||||
const yMean = ys.reduce((a, b) => a + b, 0) / ys.length;
|
||||
|
||||
let num = 0;
|
||||
let den = 0;
|
||||
for (let i = 0; i < xs.length; i++) {
|
||||
const dx = xs[i] - xMean;
|
||||
const dy = ys[i] - yMean;
|
||||
num += dx * dy;
|
||||
den += dx * dx;
|
||||
}
|
||||
return den === 0 ? 0 : num / den;
|
||||
}
|
||||
|
||||
export const ChargeVizPanel: React.FC<Props> = ({ data, width, height, options }) => {
|
||||
const theme = useTheme2();
|
||||
|
||||
const lowColour = options.lowColour || '#F44336';
|
||||
const midColour = options.midColour || '#FF9800';
|
||||
const highColour = options.highColour || '#4CAF50';
|
||||
const gradientFill = options.gradientFill ?? true;
|
||||
const dp = Number.isFinite(options.decimalPlaces) ? options.decimalPlaces : 2;
|
||||
const useRegression = options.useRegressionForRate ?? false;
|
||||
const hideText = options.hideText ?? false;
|
||||
|
||||
let currentValue = 0;
|
||||
let rate = 0;
|
||||
let low = 0;
|
||||
let high = 0;
|
||||
|
||||
// Extract and process first numeric series
|
||||
if (data.series.length > 0) {
|
||||
const series = data.series[0];
|
||||
const valueField = series.fields.find(f => f.type === 'number');
|
||||
const timeField = series.fields.find(f => f.type === 'time');
|
||||
|
||||
if (valueField && timeField) {
|
||||
const rawValues = valueField.values.toArray();
|
||||
const rawTimes = timeField.values.toArray();
|
||||
const values: number[] = [];
|
||||
const times: number[] = [];
|
||||
|
||||
for (let i = 0; i < rawValues.length; i++) {
|
||||
const v = toFinite(rawValues[i]);
|
||||
const t = toFinite(rawTimes[i]);
|
||||
if (v !== undefined && t !== undefined) {
|
||||
values.push(v);
|
||||
times.push(t);
|
||||
}
|
||||
}
|
||||
|
||||
if (values.length > 0) {
|
||||
currentValue = values[values.length - 1];
|
||||
low = Math.min(...values);
|
||||
high = Math.max(...values);
|
||||
|
||||
if (useRegression && values.length >= 2) {
|
||||
rate = slopePercentPerMinute(times, values);
|
||||
} else if (values.length >= 2) {
|
||||
const deltaPercent = values[values.length - 1] - values[0];
|
||||
const deltaMinutes = (times[times.length - 1] - times[0]) / 60000;
|
||||
rate = deltaMinutes !== 0 ? deltaPercent / deltaMinutes : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fillLevel = Math.max(0, Math.min(100, currentValue));
|
||||
let fillColour = highColour;
|
||||
if (fillLevel < 30) fillColour = lowColour;
|
||||
else if (fillLevel < 70) fillColour = midColour;
|
||||
|
||||
// Gradient fill logic
|
||||
const gradientId = 'batteryGradient';
|
||||
let gradientDef: React.ReactNode = null;
|
||||
if (gradientFill) {
|
||||
let startColour = fillColour;
|
||||
let endColour = fillColour;
|
||||
if (rate > 0) {
|
||||
startColour = fillColour;
|
||||
endColour = theme.colors.success.text;
|
||||
} else if (rate < 0) {
|
||||
startColour = theme.colors.error.text;
|
||||
endColour = fillColour;
|
||||
}
|
||||
|
||||
gradientDef = (
|
||||
<defs>
|
||||
<linearGradient id={gradientId} x1="0" y1="1" x2="0" y2="0">
|
||||
<stop offset="0%" stopColor={startColour} />
|
||||
<stop offset="100%" stopColor={endColour} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------- Dynamic scaling (Option B) ------------------------
|
||||
const aspectRatio = 0.5;
|
||||
const panelRatio = height / width;
|
||||
let batteryWidth: number;
|
||||
let batteryHeight: number;
|
||||
|
||||
if (panelRatio < aspectRatio) {
|
||||
batteryHeight = height * 0.9;
|
||||
batteryWidth = batteryHeight / aspectRatio;
|
||||
} else {
|
||||
// Use slightly less width when text visible to reduce crowding.
|
||||
batteryWidth = width * (hideText ? 0.95 : 0.6);
|
||||
batteryHeight = batteryWidth * aspectRatio;
|
||||
}
|
||||
|
||||
const fillHeightPx = (fillLevel / 100) * 220;
|
||||
|
||||
// -------------------- Render ------------------------
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: hideText ? 'center' : 'flex-start',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
color: theme.colors.text.primary,
|
||||
padding: 2, // minimal padding all round
|
||||
boxSizing: 'border-box',
|
||||
gap: hideText ? '0px' : '6px', // half the previous gap
|
||||
}}
|
||||
>
|
||||
{/* Battery SVG */}
|
||||
<svg
|
||||
style={{
|
||||
height: `${batteryHeight}px`,
|
||||
width: `${batteryWidth}px`,
|
||||
flex: '0 0 auto',
|
||||
}}
|
||||
viewBox="0 0 120 240"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
>
|
||||
{gradientDef}
|
||||
|
||||
{/* Outline */}
|
||||
<rect
|
||||
x="5"
|
||||
y="10"
|
||||
width="110"
|
||||
height="220"
|
||||
rx="8"
|
||||
ry="8"
|
||||
stroke={theme.colors.text.secondary}
|
||||
strokeWidth="3"
|
||||
fill="none"
|
||||
/>
|
||||
|
||||
{/* Terminal */}
|
||||
<rect
|
||||
x="50"
|
||||
y="0"
|
||||
width="20"
|
||||
height="8"
|
||||
rx="2"
|
||||
ry="2"
|
||||
fill={theme.colors.text.secondary}
|
||||
/>
|
||||
|
||||
{/* Fill */}
|
||||
<rect
|
||||
x="8"
|
||||
y={230 - fillHeightPx}
|
||||
width="104"
|
||||
height={fillHeightPx}
|
||||
fill={gradientFill ? `url(#${gradientId})` : fillColour}
|
||||
/>
|
||||
|
||||
{/* Text inside battery */}
|
||||
<text
|
||||
x="60"
|
||||
y="120"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
fill="#ffffff"
|
||||
fontSize="28"
|
||||
fontWeight="bold"
|
||||
>
|
||||
{fillLevel.toFixed(0)}%
|
||||
</text>
|
||||
</svg>
|
||||
|
||||
{/* Stats only if enabled */}
|
||||
{!hideText && (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
flex: '1 1 auto',
|
||||
minWidth: 120,
|
||||
overflow: 'hidden', // prevent wrapping overflow
|
||||
whiteSpace: 'nowrap', // stop premature wrapping
|
||||
textOverflow: 'ellipsis', // if too narrow, ellipsis instead
|
||||
lineHeight: '1.4em',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '1.05em', fontWeight: 600 }}>
|
||||
Rate: {rate.toFixed(dp)} %/min
|
||||
</div>
|
||||
<div>Low: {low.toFixed(dp)}%</div>
|
||||
<div>High: {high.toFixed(dp)}%</div>
|
||||
<div style={{ opacity: 0.8 }}>
|
||||
{rate > 0 ? 'Charging ↑' : rate < 0 ? 'Discharging ↓' : 'Stable'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 81.9 71.52"><defs><style>.cls-1{fill:#84aff1;}.cls-2{fill:#3865ab;}.cls-3{fill:url(#linear-gradient);}</style><linearGradient id="linear-gradient" x1="42.95" y1="16.88" x2="81.9" y2="16.88" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f2cc0c"/><stop offset="1" stop-color="#ff9830"/></linearGradient></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M55.46,62.43A2,2,0,0,1,54.07,59l4.72-4.54a2,2,0,0,1,2.2-.39l3.65,1.63,3.68-3.64a2,2,0,1,1,2.81,2.84l-4.64,4.6a2,2,0,0,1-2.22.41L60.6,58.26l-3.76,3.61A2,2,0,0,1,55.46,62.43Z"/><path class="cls-2" d="M37,0H2A2,2,0,0,0,0,2V31.76a2,2,0,0,0,2,2H37a2,2,0,0,0,2-2V2A2,2,0,0,0,37,0ZM4,29.76V8.84H35V29.76Z"/><path class="cls-3" d="M79.9,0H45a2,2,0,0,0-2,2V31.76a2,2,0,0,0,2,2h35a2,2,0,0,0,2-2V2A2,2,0,0,0,79.9,0ZM47,29.76V8.84h31V29.76Z"/><path class="cls-2" d="M37,37.76H2a2,2,0,0,0-2,2V69.52a2,2,0,0,0,2,2H37a2,2,0,0,0,2-2V39.76A2,2,0,0,0,37,37.76ZM4,67.52V46.6H35V67.52Z"/><path class="cls-2" d="M79.9,37.76H45a2,2,0,0,0-2,2V69.52a2,2,0,0,0,2,2h35a2,2,0,0,0,2-2V39.76A2,2,0,0,0,79.9,37.76ZM47,67.52V46.6h31V67.52Z"/><rect class="cls-1" x="10.48" y="56.95" width="4" height="5.79"/><rect class="cls-1" x="17.43" y="53.95" width="4" height="8.79"/><rect class="cls-1" x="24.47" y="50.95" width="4" height="11.79"/><path class="cls-1" d="M19.47,25.8a6.93,6.93,0,1,1,6.93-6.92A6.93,6.93,0,0,1,19.47,25.8Zm0-9.85a2.93,2.93,0,1,0,2.93,2.93A2.93,2.93,0,0,0,19.47,16Z"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,87 @@
|
||||
// Import PanelPlugin to register our React component as a Grafana panel.
|
||||
import { PanelPlugin } from '@grafana/data';
|
||||
// Import the component itself.
|
||||
import { ChargeVizPanel } from './components/ChargeVizPanel';
|
||||
|
||||
/**
|
||||
* Re-declare the options interface here to keep this file self-contained for the editor.
|
||||
* These must match the props used in ChargeVizPanel.
|
||||
*/
|
||||
interface ChargeVizOptions {
|
||||
lowColour: string;
|
||||
midColour: string;
|
||||
highColour: string;
|
||||
gradientFill: boolean;
|
||||
decimalPlaces: number;
|
||||
useRegressionForRate: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export the plugin instance that Grafana will discover.
|
||||
* We also define the panel editor options so users can configure colours and behaviour.
|
||||
*/
|
||||
export const plugin = new PanelPlugin<ChargeVizOptions>(ChargeVizPanel).setPanelOptions(
|
||||
// The builder lets us declare editor controls (colour pickers, switches, number inputs).
|
||||
(builder) => {
|
||||
// Colour: low battery
|
||||
builder.addColorPicker({
|
||||
path: 'lowColour', // option field name
|
||||
name: 'Low battery colour', // label in the editor
|
||||
defaultValue: '#F44336', // sensible default (red)
|
||||
description: 'Colour when the level is under 30%.',
|
||||
});
|
||||
|
||||
// Colour: medium battery
|
||||
builder.addColorPicker({
|
||||
path: 'midColour',
|
||||
name: 'Medium battery colour',
|
||||
defaultValue: '#FF9800', // amber
|
||||
description: 'Colour when the level is between 30% and 70%.',
|
||||
});
|
||||
|
||||
// Colour: high battery
|
||||
builder.addColorPicker({
|
||||
path: 'highColour',
|
||||
name: 'High battery colour',
|
||||
defaultValue: '#4CAF50', // green
|
||||
description: 'Colour when the level is above 70%.',
|
||||
});
|
||||
|
||||
// Toggle for gradient fill.
|
||||
builder.addBooleanSwitch({
|
||||
path: 'gradientFill',
|
||||
name: 'Enable gradient fill',
|
||||
defaultValue: true,
|
||||
description: 'Show a subtle gradient that hints charging/discharging.',
|
||||
});
|
||||
|
||||
// Numeric input: decimal places for numbers shown in the stats.
|
||||
builder.addNumberInput({
|
||||
path: 'decimalPlaces',
|
||||
name: 'Decimal places',
|
||||
defaultValue: 2,
|
||||
settings: { min: 0, max: 6, integer: true },
|
||||
description: 'How many decimal places to use for rate/low/high.',
|
||||
});
|
||||
|
||||
builder.addBooleanSwitch({
|
||||
path: 'hideText',
|
||||
name: 'Hide stats text (battery only)',
|
||||
defaultValue: false,
|
||||
description: 'If enabled, hides the rate and high/low text, showing only the battery graphic.',
|
||||
});
|
||||
|
||||
|
||||
// Toggle: use regression for rate instead of simple first/last.
|
||||
builder.addBooleanSwitch({
|
||||
path: 'useRegressionForRate',
|
||||
name: 'Use regression for rate',
|
||||
defaultValue: false,
|
||||
description:
|
||||
'Compute %/min using more complicated maths.',
|
||||
});
|
||||
|
||||
// Return the builder so Grafana applies the options.
|
||||
return builder;
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json",
|
||||
"type": "panel",
|
||||
"name": "Battery",
|
||||
"id": "andydixon-battery-panel",
|
||||
"info": {
|
||||
"keywords": ["panel"],
|
||||
"description": "",
|
||||
"author": {
|
||||
"name": "Andy dixon"
|
||||
},
|
||||
"logos": {
|
||||
"small": "img/logo.svg",
|
||||
"large": "img/logo.svg"
|
||||
},
|
||||
"links": [],
|
||||
"screenshots": [],
|
||||
"version": "%VERSION%",
|
||||
"updated": "%TODAY%"
|
||||
},
|
||||
"dependencies": {
|
||||
"grafanaDependency": ">=10.4.0",
|
||||
"plugins": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
type SeriesSize = 'sm' | 'md' | 'lg';
|
||||
|
||||
export interface SimpleOptions {
|
||||
text: string;
|
||||
showSeriesCount: boolean;
|
||||
seriesCountSize: SeriesSize;
|
||||
}
|
||||
Reference in New Issue
Block a user