Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
N
nexpie-grafana-theme
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Registry
Registry
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Kornkitt Poolsup
nexpie-grafana-theme
Commits
63caedb0
Commit
63caedb0
authored
Sep 28, 2016
by
Torkel Ödegaard
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'master' of github.com:grafana/grafana
parents
460160cf
262e7193
Hide whitespace changes
Inline
Side-by-side
Showing
21 changed files
with
202 additions
and
47 deletions
+202
-47
CHANGELOG.md
+2
-0
conf/defaults.ini
+9
-0
conf/sample.ini
+6
-0
docs/sources/installation/configuration.md
+6
-0
pkg/api/render.go
+1
-1
pkg/cmd/grafana-server/main.go
+2
-1
pkg/components/renderer/renderer.go
+4
-4
pkg/models/timer.go
+7
-0
pkg/plugins/update_checker.go
+17
-1
pkg/services/alerting/conditions/query.go
+2
-1
pkg/services/alerting/engine.go
+8
-4
pkg/services/alerting/eval_context.go
+2
-11
pkg/services/alerting/eval_handler.go
+1
-1
pkg/services/alerting/notifier.go
+8
-7
pkg/services/alerting/notifiers/webhook.go
+2
-3
pkg/services/backgroundtasks/background_tasks.go
+39
-0
pkg/services/backgroundtasks/remove_tmp_images.go
+38
-0
pkg/services/sqlstore/dashboard_snapshot.go
+27
-0
pkg/setting/setting.go
+13
-5
pkg/tsdb/graphite/graphite.go
+4
-4
public/views/index.html
+4
-4
No files found.
CHANGELOG.md
View file @
63caedb0
...
...
@@ -14,6 +14,8 @@
*
**OAuth**
: Add support for generic oauth, closes
[
#4718
](
https://github.com/grafana/grafana/pull/4718
)
*
**Cloudwatch**
: Add support to expand multi select template variable, closes
[
#5003
](
https://github.com/grafana/grafana/pull/5003
)
*
**Graph Panel**
: Now supports flexible lower/upper bounds on Y-Max and Y-Min, PR
[
#5720
](
https://github.com/grafana/grafana/pull/5720
)
*
**Background Tasks**
: Now support automatic purging of old snapshots, closes
[
#4087
](
https://github.com/grafana/grafana/issues/4087
)
*
**Background Tasks**
: Now support automatic purging of old rendered images, closes
[
#2172
](
https://github.com/grafana/grafana/issues/2172
)
### Breaking changes
*
**SystemD**
: Change systemd description, closes
[
#5971
](
https://github.com/grafana/grafana/pull/5971
)
...
...
conf/defaults.ini
View file @
63caedb0
...
...
@@ -161,6 +161,12 @@ external_enabled = true
external_snapshot_url
=
https://snapshots-origin.raintank.io
external_snapshot_name
=
Publish to snapshot.raintank.io
# remove expired snapshot
snapshot_remove_expired
=
true
# remove snapshots after 90 days
snapshot_TTL_days
=
90
#################################### Users ####################################
[users]
# disable user signup / registration
...
...
@@ -267,6 +273,9 @@ from_address = admin@grafana.localhost
welcome_email_on_sign_up
=
false
templates_pattern
=
emails/*.html
[tmp.files]
rendered_image_ttl_days
=
14
#################################### Logging ##########################
[log]
# Either "console", "file", "syslog". Default is console and file
...
...
conf/sample.ini
View file @
63caedb0
...
...
@@ -149,6 +149,12 @@ check_for_updates = true
;external_snapshot_url = https://snapshots-origin.raintank.io
;external_snapshot_name = Publish to snapshot.raintank.io
# remove expired snapshot
;snapshot_remove_expired = true
# remove snapshots after 90 days
;snapshot_TTL_days = 90
#################################### Users ####################################
[users]
# disable user signup / registration
...
...
docs/sources/installation/configuration.md
View file @
63caedb0
...
...
@@ -525,3 +525,9 @@ Set root url to a Grafana instance where you want to publish external snapshots
### external_snapshot_name
Set name for external snapshot button. Defaults to
`Publish to snapshot.raintank.io`
### remove expired snapshot
Enabled to automatically remove expired snapshots
### remove snapshots after 90 days
Time to live for snapshots.
pkg/api/render.go
View file @
63caedb0
...
...
@@ -14,7 +14,7 @@ func RenderToPng(c *middleware.Context) {
queryParams
:=
fmt
.
Sprintf
(
"?%s"
,
c
.
Req
.
URL
.
RawQuery
)
renderOpts
:=
&
renderer
.
RenderOpts
{
Url
:
c
.
Params
(
"*"
)
+
queryParams
,
Path
:
c
.
Params
(
"*"
)
+
queryParams
,
Width
:
queryReader
.
Get
(
"width"
,
"800"
),
Height
:
queryReader
.
Get
(
"height"
,
"400"
),
OrgId
:
c
.
OrgId
,
...
...
pkg/cmd/grafana-server/main.go
View file @
63caedb0
...
...
@@ -17,6 +17,7 @@ import (
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/plugins"
alertingInit
"github.com/grafana/grafana/pkg/services/alerting/init"
"github.com/grafana/grafana/pkg/services/backgroundtasks"
"github.com/grafana/grafana/pkg/services/eventpublisher"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/services/search"
...
...
@@ -62,13 +63,13 @@ func main() {
writePIDFile
()
initRuntime
()
metrics
.
Init
()
search
.
Init
()
login
.
Init
()
social
.
NewOAuthService
()
eventpublisher
.
Init
()
plugins
.
Init
()
alertingInit
.
Init
()
backgroundtasks
.
Init
()
if
err
:=
notifications
.
Init
();
err
!=
nil
{
log
.
Fatal
(
3
,
"Notification service failed to initialize"
,
err
)
...
...
pkg/components/renderer/renderer.go
View file @
63caedb0
...
...
@@ -18,7 +18,7 @@ import (
)
type
RenderOpts
struct
{
Url
string
Path
string
Width
string
Height
string
Timeout
string
...
...
@@ -28,14 +28,14 @@ type RenderOpts struct {
var
rendererLog
log
.
Logger
=
log
.
New
(
"png-renderer"
)
func
RenderToPng
(
params
*
RenderOpts
)
(
string
,
error
)
{
rendererLog
.
Info
(
"Rendering"
,
"
url"
,
params
.
Url
)
rendererLog
.
Info
(
"Rendering"
,
"
path"
,
params
.
Path
)
var
executable
=
"phantomjs"
if
runtime
.
GOOS
==
"windows"
{
executable
=
executable
+
".exe"
}
params
.
Url
=
fmt
.
Sprintf
(
"%s://localhost:%s/%s"
,
setting
.
Protocol
,
setting
.
HttpPort
,
params
.
Url
)
url
:=
fmt
.
Sprintf
(
"%s://localhost:%s/%s"
,
setting
.
Protocol
,
setting
.
HttpPort
,
params
.
Path
)
binPath
,
_
:=
filepath
.
Abs
(
filepath
.
Join
(
setting
.
PhantomDir
,
executable
))
scriptPath
,
_
:=
filepath
.
Abs
(
filepath
.
Join
(
setting
.
PhantomDir
,
"render.js"
))
...
...
@@ -48,7 +48,7 @@ func RenderToPng(params *RenderOpts) (string, error) {
cmdArgs
:=
[]
string
{
"--ignore-ssl-errors=true"
,
scriptPath
,
"url="
+
params
.
U
rl
,
"url="
+
u
rl
,
"width="
+
params
.
Width
,
"height="
+
params
.
Height
,
"png="
+
pngPath
,
...
...
pkg/models/timer.go
0 → 100644
View file @
63caedb0
package
models
import
"time"
type
HourCommand
struct
{
Time
time
.
Time
}
pkg/plugins/update_checker.go
View file @
63caedb0
...
...
@@ -9,6 +9,7 @@ import (
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/setting"
"github.com/hashicorp/go-version"
)
var
(
...
...
@@ -85,7 +86,15 @@ func checkForUpdates() {
for
_
,
gplug
:=
range
gNetPlugins
{
if
gplug
.
Slug
==
plug
.
Id
{
plug
.
GrafanaNetVersion
=
gplug
.
Version
plug
.
GrafanaNetHasUpdate
=
plug
.
Info
.
Version
!=
plug
.
GrafanaNetVersion
plugVersion
,
err1
:=
version
.
NewVersion
(
plug
.
Info
.
Version
)
gplugVersion
,
err2
:=
version
.
NewVersion
(
gplug
.
Version
)
if
err1
!=
nil
||
err2
!=
nil
{
plug
.
GrafanaNetHasUpdate
=
plug
.
Info
.
Version
!=
plug
.
GrafanaNetVersion
}
else
{
plug
.
GrafanaNetHasUpdate
=
plugVersion
.
LessThan
(
gplugVersion
)
}
}
}
}
...
...
@@ -117,4 +126,11 @@ func checkForUpdates() {
GrafanaLatestVersion
=
githubLatest
.
Stable
GrafanaHasUpdate
=
githubLatest
.
Stable
!=
setting
.
BuildVersion
}
currVersion
,
err1
:=
version
.
NewVersion
(
setting
.
BuildVersion
)
latestVersion
,
err2
:=
version
.
NewVersion
(
GrafanaLatestVersion
)
if
err1
==
nil
&&
err2
==
nil
{
GrafanaHasUpdate
=
currVersion
.
LessThan
(
latestVersion
)
}
}
pkg/services/alerting/conditions/query.go
View file @
63caedb0
...
...
@@ -184,5 +184,6 @@ func validateToValue(to string) error {
}
}
return
fmt
.
Errorf
(
"cannot parse to value %s"
,
to
)
_
,
err
:=
time
.
ParseDuration
(
to
)
return
err
}
pkg/services/alerting/engine.go
View file @
63caedb0
...
...
@@ -93,14 +93,18 @@ func (e *Engine) executeJob(job *Job) {
}
func
(
e
*
Engine
)
resultDispatcher
()
{
for
result
:=
range
e
.
resultQueue
{
go
e
.
handleResponse
(
result
)
}
}
func
(
e
*
Engine
)
handleResponse
(
result
*
EvalContext
)
{
defer
func
()
{
if
err
:=
recover
();
err
!=
nil
{
e
.
log
.
Error
(
"Panic in resultDispatcher"
,
"error"
,
err
,
"stack"
,
log
.
Stack
(
1
))
}
}()
for
result
:=
range
e
.
resultQueue
{
e
.
log
.
Debug
(
"Alert Rule Result"
,
"ruleId"
,
result
.
Rule
.
Id
,
"firing"
,
result
.
Firing
)
e
.
resultHandler
.
Handle
(
result
)
}
e
.
log
.
Debug
(
"Alert Rule Result"
,
"ruleId"
,
result
.
Rule
.
Id
,
"firing"
,
result
.
Firing
)
e
.
resultHandler
.
Handle
(
result
)
}
pkg/services/alerting/eval_context.go
View file @
63caedb0
...
...
@@ -71,7 +71,7 @@ func (c *EvalContext) GetNotificationTitle() string {
return
"["
+
c
.
GetStateModel
()
.
Text
+
"] "
+
c
.
Rule
.
Name
}
func
(
c
*
EvalContext
)
g
etDashboardSlug
()
(
string
,
error
)
{
func
(
c
*
EvalContext
)
G
etDashboardSlug
()
(
string
,
error
)
{
if
c
.
dashboardSlug
!=
""
{
return
c
.
dashboardSlug
,
nil
}
...
...
@@ -86,7 +86,7 @@ func (c *EvalContext) getDashboardSlug() (string, error) {
}
func
(
c
*
EvalContext
)
GetRuleUrl
()
(
string
,
error
)
{
if
slug
,
err
:=
c
.
g
etDashboardSlug
();
err
!=
nil
{
if
slug
,
err
:=
c
.
G
etDashboardSlug
();
err
!=
nil
{
return
""
,
err
}
else
{
ruleUrl
:=
fmt
.
Sprintf
(
"%sdashboard/db/%s?fullscreen&edit&tab=alert&panelId=%d"
,
setting
.
AppUrl
,
slug
,
c
.
Rule
.
PanelId
)
...
...
@@ -94,15 +94,6 @@ func (c *EvalContext) GetRuleUrl() (string, error) {
}
}
func
(
c
*
EvalContext
)
GetImageUrl
()
(
string
,
error
)
{
if
slug
,
err
:=
c
.
getDashboardSlug
();
err
!=
nil
{
return
""
,
err
}
else
{
ruleUrl
:=
fmt
.
Sprintf
(
"%sdashboard-solo/db/%s?&panelId=%d"
,
setting
.
AppUrl
,
slug
,
c
.
Rule
.
PanelId
)
return
ruleUrl
,
nil
}
}
func
NewEvalContext
(
rule
*
Rule
)
*
EvalContext
{
return
&
EvalContext
{
StartTime
:
time
.
Now
(),
...
...
pkg/services/alerting/eval_handler.go
View file @
63caedb0
...
...
@@ -20,7 +20,7 @@ type DefaultEvalHandler struct {
func
NewEvalHandler
()
*
DefaultEvalHandler
{
return
&
DefaultEvalHandler
{
log
:
log
.
New
(
"alerting.evalHandler"
),
alertJobTimeout
:
time
.
Second
*
1
0
,
alertJobTimeout
:
time
.
Second
*
1
5
,
}
}
...
...
pkg/services/alerting/notifier.go
View file @
63caedb0
...
...
@@ -2,6 +2,7 @@ package alerting
import
(
"errors"
"fmt"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/imguploader"
...
...
@@ -60,22 +61,22 @@ func (n *RootNotifier) sendNotifications(notifiers []Notifier, context *EvalCont
}
}
func
(
n
*
RootNotifier
)
uploadImage
(
context
*
EvalContext
)
error
{
func
(
n
*
RootNotifier
)
uploadImage
(
context
*
EvalContext
)
(
err
error
)
{
uploader
,
_
:=
imguploader
.
NewImageUploader
()
imageUrl
,
err
:=
context
.
GetImageUrl
()
if
err
!=
nil
{
return
err
}
renderOpts
:=
&
renderer
.
RenderOpts
{
Url
:
imageUrl
,
Width
:
"800"
,
Height
:
"400"
,
Timeout
:
"30"
,
OrgId
:
context
.
Rule
.
OrgId
,
}
if
slug
,
err
:=
context
.
GetDashboardSlug
();
err
!=
nil
{
return
err
}
else
{
renderOpts
.
Path
=
fmt
.
Sprintf
(
"dashboard-solo/db/%s?&panelId=%d"
,
slug
,
context
.
Rule
.
PanelId
)
}
if
imagePath
,
err
:=
renderer
.
RenderToPng
(
renderOpts
);
err
!=
nil
{
return
err
}
else
{
...
...
pkg/services/alerting/notifiers/webhook.go
View file @
63caedb0
...
...
@@ -52,9 +52,8 @@ func (this *WebhookNotifier) Notify(context *alerting.EvalContext) {
bodyJSON
.
Set
(
"rule_url"
,
ruleUrl
)
}
imageUrl
,
err
:=
context
.
GetImageUrl
()
if
err
==
nil
{
bodyJSON
.
Set
(
"image_url"
,
imageUrl
)
if
context
.
ImagePublicUrl
!=
""
{
bodyJSON
.
Set
(
"image_url"
,
context
.
ImagePublicUrl
)
}
body
,
_
:=
bodyJSON
.
MarshalJSON
()
...
...
pkg/services/backgroundtasks/background_tasks.go
0 → 100644
View file @
63caedb0
//"I want to be a cleaner, just like you," said Mathilda
//"Okay," replied Leon
package
backgroundtasks
import
(
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/models"
)
var
(
tlog
log
.
Logger
=
log
.
New
(
"ticker"
)
)
func
Init
()
{
go
start
()
}
func
start
()
{
go
cleanup
(
time
.
Now
())
ticker
:=
time
.
NewTicker
(
time
.
Hour
*
1
)
for
{
select
{
case
tick
:=
<-
ticker
.
C
:
go
cleanup
(
tick
)
}
}
}
func
cleanup
(
now
time
.
Time
)
{
err
:=
bus
.
Publish
(
&
models
.
HourCommand
{
Time
:
now
})
if
err
!=
nil
{
tlog
.
Error
(
"Cleanup job failed"
,
"error"
,
err
)
}
}
pkg/services/backgroundtasks/remove_tmp_images.go
0 → 100644
View file @
63caedb0
package
backgroundtasks
import
(
"io/ioutil"
"os"
"path"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
)
func
init
()
{
bus
.
AddEventListener
(
CleanTmpFiles
)
}
func
CleanTmpFiles
(
cmd
*
models
.
HourCommand
)
error
{
files
,
err
:=
ioutil
.
ReadDir
(
setting
.
ImagesDir
)
var
toDelete
[]
os
.
FileInfo
for
_
,
file
:=
range
files
{
if
file
.
ModTime
()
.
AddDate
(
0
,
0
,
setting
.
RenderedImageTTLDays
)
.
Before
(
cmd
.
Time
)
{
toDelete
=
append
(
toDelete
,
file
)
}
}
for
_
,
file
:=
range
toDelete
{
fullPath
:=
path
.
Join
(
setting
.
ImagesDir
,
file
.
Name
())
err
:=
os
.
Remove
(
fullPath
)
if
err
!=
nil
{
return
err
}
}
tlog
.
Debug
(
"Found old rendered image to delete"
,
"deleted"
,
len
(
toDelete
),
"keept"
,
len
(
files
))
return
err
}
pkg/services/sqlstore/dashboard_snapshot.go
View file @
63caedb0
...
...
@@ -5,7 +5,9 @@ import (
"github.com/go-xorm/xorm"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
m
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
)
func
init
()
{
...
...
@@ -13,6 +15,31 @@ func init() {
bus
.
AddHandler
(
"sql"
,
GetDashboardSnapshot
)
bus
.
AddHandler
(
"sql"
,
DeleteDashboardSnapshot
)
bus
.
AddHandler
(
"sql"
,
SearchDashboardSnapshots
)
bus
.
AddEventListener
(
DeleteExpiredSnapshots
)
}
func
DeleteExpiredSnapshots
(
cmd
*
m
.
HourCommand
)
error
{
return
inTransaction
(
func
(
sess
*
xorm
.
Session
)
error
{
var
expiredCount
int64
=
0
var
oldCount
int64
=
0
if
setting
.
SnapShotRemoveExpired
{
deleteExpiredSql
:=
"DELETE FROM dashboard_snapshot WHERE expires < ?"
expiredResponse
,
err
:=
x
.
Exec
(
deleteExpiredSql
,
cmd
.
Time
)
if
err
!=
nil
{
return
err
}
expiredCount
,
_
=
expiredResponse
.
RowsAffected
()
}
oldSnapshotsSql
:=
"DELETE FROM dashboard_snapshot WHERE created < ?"
oldResponse
,
err
:=
x
.
Exec
(
oldSnapshotsSql
,
cmd
.
Time
.
AddDate
(
0
,
0
,
setting
.
SnapShotTTLDays
*-
1
))
oldCount
,
_
=
oldResponse
.
RowsAffected
()
log
.
Debug2
(
"Deleted old/expired snaphots"
,
"to old"
,
oldCount
,
"expired"
,
expiredCount
)
return
err
})
}
func
CreateDashboardSnapshot
(
cmd
*
m
.
CreateDashboardSnapshotCommand
)
error
{
...
...
pkg/setting/setting.go
View file @
63caedb0
...
...
@@ -78,9 +78,11 @@ var (
DataProxyWhiteList
map
[
string
]
bool
// Snapshots
ExternalSnapshotUrl
string
ExternalSnapshotName
string
ExternalEnabled
bool
ExternalSnapshotUrl
string
ExternalSnapshotName
string
ExternalEnabled
bool
SnapShotTTLDays
int
SnapShotRemoveExpired
bool
// User settings
AllowUserSignUp
bool
...
...
@@ -118,8 +120,9 @@ var (
IsWindows
bool
// PhantomJs Rendering
ImagesDir
string
PhantomDir
string
ImagesDir
string
PhantomDir
string
RenderedImageTTLDays
int
// for logging purposes
configFiles
[]
string
...
...
@@ -495,6 +498,8 @@ func NewConfigContext(args *CommandLineArgs) error {
ExternalSnapshotUrl
=
snapshots
.
Key
(
"external_snapshot_url"
)
.
String
()
ExternalSnapshotName
=
snapshots
.
Key
(
"external_snapshot_name"
)
.
String
()
ExternalEnabled
=
snapshots
.
Key
(
"external_enabled"
)
.
MustBool
(
true
)
SnapShotRemoveExpired
=
snapshots
.
Key
(
"snapshot_remove_expired"
)
.
MustBool
(
true
)
SnapShotTTLDays
=
snapshots
.
Key
(
"snapshot_TTL_days"
)
.
MustInt
(
90
)
// read data source proxy white list
DataProxyWhiteList
=
make
(
map
[
string
]
bool
)
...
...
@@ -535,6 +540,9 @@ func NewConfigContext(args *CommandLineArgs) error {
ImagesDir
=
filepath
.
Join
(
DataPath
,
"png"
)
PhantomDir
=
filepath
.
Join
(
HomePath
,
"vendor/phantomjs"
)
tmpFilesSection
:=
Cfg
.
Section
(
"tmp.files"
)
RenderedImageTTLDays
=
tmpFilesSection
.
Key
(
"rendered_image_ttl_days"
)
.
MustInt
(
14
)
analytics
:=
Cfg
.
Section
(
"analytics"
)
ReportingEnabled
=
analytics
.
Key
(
"reporting_enabled"
)
.
MustBool
(
true
)
CheckForUpdates
=
analytics
.
Key
(
"check_for_updates"
)
.
MustBool
(
true
)
...
...
pkg/tsdb/graphite/graphite.go
View file @
63caedb0
...
...
@@ -38,7 +38,7 @@ func init() {
}
HttpClient
=
http
.
Client
{
Timeout
:
time
.
Duration
(
1
0
*
time
.
Second
),
Timeout
:
time
.
Duration
(
1
5
*
time
.
Second
),
Transport
:
tr
,
}
}
...
...
@@ -102,9 +102,9 @@ func (e *GraphiteExecutor) parseResponse(res *http.Response) ([]TargetResponseDT
return
nil
,
err
}
if
res
.
StatusCode
==
http
.
StatusUnauthorized
{
glog
.
Info
(
"Request
is Unauthoriz
ed"
,
"status"
,
res
.
Status
,
"body"
,
string
(
body
))
return
nil
,
fmt
.
Errorf
(
"Request
is Unauthorized status: %v body: %s"
,
res
.
Status
,
string
(
body
)
)
if
res
.
StatusCode
/
100
!=
2
{
glog
.
Info
(
"Request
fail
ed"
,
"status"
,
res
.
Status
,
"body"
,
string
(
body
))
return
nil
,
fmt
.
Errorf
(
"Request
failed status: %v"
,
res
.
Status
)
}
var
data
[]
TargetResponseDTO
...
...
public/views/index.html
View file @
63caedb0
...
...
@@ -64,13 +64,13 @@
<a
href=
"http://grafana.org"
target=
"_blank"
>
Grafana
</a>
<span>
v[[.BuildVersion]] (commit: [[.BuildCommit]])
</span>
</li>
<li>
[[if .NewGrafanaVersionExists]]
[[if .NewGrafanaVersionExists]]
<li>
<a
href=
"http://grafana.org/download"
target=
"_blank"
bs-tooltip=
"'[[.NewGrafanaVersion]]'"
>
New version available!
</a>
[[end]]
</li>
</li>
[[end]]
</ul>
</div>
</footer>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment