Last active
August 6, 2023 07:25
-
-
Save gordonturner/039abb1b6bdfcd071fcfa98953b92f2e to your computer and use it in GitHub Desktop.
Custom Grafana Plugin
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useEffect } from 'react'; | |
import { PanelProps } from '@grafana/data'; | |
import { stylesFactory } from '@grafana/ui'; | |
import { css, cx, injectGlobal } from 'emotion'; | |
import glyphiconWoff from './fonts/glyphicons-halflings-regular.woff'; | |
import { DEFAULT_API_URL, DEFAULT_API_KEY } from './constants'; | |
import { ListlistData, ListlistItem, ListlistOptions } from './types'; | |
import { DateTime } from 'luxon'; | |
import axios, { AxiosRequestConfig } from 'axios'; | |
interface Props extends PanelProps<ListlistOptions> {} | |
export const ListlistPanel: React.FC<Props> = ({ options, data, width, height }) => { | |
const styles = getStyles(); | |
const [listlistData, updateListlistData] = React.useState<ListlistData>(); | |
// HACK: Trick useEffect() by incrementing a counter on a timer. | |
const [count, setCount] = React.useState(0); | |
// Logging when this is called by Grafana. | |
console.log("Grafana called..."); | |
// setCount(count + 1); | |
/** | |
* This function manages the async call to the API. | |
* | |
* curl 'https://prod.gordonturner.com/listlist-web/api/2' \ | |
* -H 'api-key: XXXX' | |
*/ | |
const requestData = async () => { | |
// Setup the authentication header | |
const requestOptions: AxiosRequestConfig = { | |
method: 'GET', | |
headers: { | |
'api-key': options.apiKey ? options.apiKey : DEFAULT_API_KEY, | |
}, | |
}; | |
const response = axios.get(options.apiUrl ? options.apiUrl : DEFAULT_API_URL, requestOptions); | |
const data = (await response).data; | |
console.log('Data updated:'); | |
console.log(data); | |
if (data.list === undefined || data.list.length == 0) { | |
// Show a 'No Active Items' message | |
const item: ListlistItem = { | |
id: 1, | |
state: 'NONE', | |
name: 'No Active Items ', | |
sortOrder: 1, | |
createDate: '2222-01-01T00:00:00.000+0000', | |
dueDate: '2222-01-01T00:00:00.000+0000', | |
completedDate: '2222-01-01T00:00:00.000+0000', | |
}; | |
const listlistDataNoActiveItems: ListlistData = { | |
list: [item] | |
}; | |
updateListlistData(listlistDataNoActiveItems); | |
} else { | |
updateListlistData(data); | |
} | |
}; | |
/** | |
* Accepts a function that contains imperative, possibly effectful code. | |
* | |
* Mutations, subscriptions, timers, logging, and other side effects are not allowed | |
* inside the main body of a function component (referred to as React’s render phase). | |
* Doing so will lead to confusing bugs and inconsistencies in the UI. | |
* | |
* Instead, use useEffect. The function passed to useEffect will run after the render | |
* is committed to the screen. Think of effects as an escape hatch from React’s purely | |
* functional world into the imperative world. | |
* | |
* - Reference: | |
* https://reactjs.org/docs/hooks-reference.html#useeffect | |
* | |
* In this useEffect hook, an empty array [] is the second argument so the code inside | |
* useEffect will run only once when the component is mounted. This is functionally | |
* similar to componentDidMount lifecycle method in react hooks. | |
*/ | |
useEffect(() => { | |
const requestDataAsync = async () => { | |
await requestData(); | |
}; | |
requestDataAsync(); | |
setTimeout(() => { | |
setCount(count + 1); | |
console.log(count); | |
}, 30000); | |
}, [count]); | |
return ( | |
<div | |
className={cx( | |
styles.wrapper, | |
css` | |
width: ${width}px; | |
height: ${height}px; | |
` | |
)} | |
> | |
<h1 className={styles.listName}>{listlistData?.name}</h1> | |
<ul id="start-page-todo--todo" className={styles.listGroup}> | |
{listlistData?.list?.map(listlist => ( | |
<li className={styles.listGroupItem} key={listlist.id}> | |
<span | |
className={[ | |
styles.glyphicon, | |
getGlyphiconIconClass(listlist.state), | |
getGlyphiconColorClass(listlist.createDate), | |
].join(' ')} | |
></span> | |
<span className={styles.activeListItemName}>{listlist.name}</span> | |
</li> | |
))} | |
</ul> | |
</div> | |
); | |
}; | |
/* | |
* Add font for the check icons. | |
*/ | |
injectGlobal` | |
@font-face { | |
font-family: 'Glyphicons Halflings'; | |
font-style: normal; | |
font-weight: normal; | |
src: url('${glyphiconWoff}') | |
format('woff'); | |
}`; | |
const getStyles = stylesFactory(() => { | |
return { | |
wrapper: css` | |
position: relative; | |
`, | |
svg: css` | |
position: absolute; | |
top: 0; | |
left: 0; | |
`, | |
textBox: css` | |
position: absolute; | |
bottom: 0; | |
left: 0; | |
padding: 10px; | |
`, | |
listName: css` | |
margin-left: 10%; | |
`, | |
listGroup: css` | |
list-style: none; | |
margin-left: 2%; | |
`, | |
listGroupItem: css` | |
border-radius: 0 !important; | |
font-size: 25px; | |
line-height: 1.5; | |
margin-left: 50px; | |
`, | |
glyphicon: css` | |
display: inline-block; | |
margin-top: 3px; | |
vertical-align: text-top; | |
position: relative; | |
top: 3px; | |
display: inline-block; | |
-webkit-font-smoothing: antialiased; | |
font-style: normal; | |
font-weight: normal; | |
line-height: 1; | |
font-family: 'Glyphicons Halflings'; | |
`, | |
glyphiconColorNormal: css` | |
color: inherit; | |
`, | |
glyphiconColorGreen: css` | |
color: green; | |
`, | |
glyphiconColorOrange: css` | |
color: orange; | |
`, | |
glyphiconColorRed: css` | |
color: red; | |
`, | |
glyphiconChecked: css` | |
:before { | |
content: '\\e067'; | |
} | |
`, | |
glyphiconUnchecked: css` | |
:before { | |
content: '\\e157'; | |
} | |
`, | |
glyphiconNoActiveItems: css` | |
:before { | |
content: '\\e162'; | |
} | |
`, | |
activeListItemName: css` | |
display: inline-block; | |
vertical-align: text-top; | |
width: 89%; | |
margin-left: 10px; | |
margin-top: 4px; | |
margin-bottom: 5px; | |
`, | |
}; | |
}); | |
/* | |
* Function that will return the appropriate class for create date. | |
*/ | |
function getGlyphiconColorClass(createDate: string) { | |
let diffDate: number = DateTime.local().toMillis() - DateTime.fromISO(createDate).toMillis(); | |
// console.log(createDate); | |
// console.log(diffDate); | |
if (diffDate < 0) { | |
// console.log('No Active Items'); | |
return getStyles().glyphiconColorNormal; | |
} else if (diffDate < 604800000) { | |
// console.log('less then a week, green'); | |
return getStyles().glyphiconColorGreen; | |
} else if (diffDate > 604800000 && diffDate < 1814400000) { | |
// console.log('more then a week, less then 3, orange'); | |
return getStyles().glyphiconColorOrange; | |
} else { | |
// console.log('more then 3 weeks, red'); | |
return getStyles().glyphiconColorRed; | |
} | |
} | |
/* | |
* Function that will return the appropriate class for icon. | |
*/ | |
function getGlyphiconIconClass(state: string) { | |
if ('ACTIVE' == state) { | |
// console.log('ACTIVE, setting icon to unchecked'); | |
return getStyles().glyphiconUnchecked; | |
} else if ('COMPLETED' == state) { | |
// console.log('COMPLETED, setting icon to checked'); | |
return getStyles().glyphiconChecked; | |
} else { | |
// console.log('Empty, setting no icon'); | |
return getStyles().glyphiconNoActiveItems; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment