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
a826591e
Commit
a826591e
authored
Oct 19, 2016
by
Torkel Ödegaard
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'master' of github.com:grafana/grafana
parents
a8dd4491
20bfe443
Show whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
138 additions
and
91 deletions
+138
-91
appveyor.yml
+2
-1
build.go
+3
-0
docker/blocks/influxdb/config.toml
+0
-75
docker/blocks/influxdb/fig
+2
-1
docker/blocks/influxdb/influxdb.conf
+92
-0
pkg/api/api.go
+2
-2
pkg/models/notifications.go
+2
-0
pkg/services/alerting/notifiers/webhook.go
+3
-0
pkg/services/notifications/notifications.go
+2
-0
pkg/services/notifications/webhook.go
+7
-2
pkg/social/google_oauth.go
+1
-0
pkg/tsdb/influxdb/influxdb.go
+5
-0
pkg/tsdb/prometheus/prometheus.go
+4
-2
pkg/tsdb/prometheus/prometheus_test.go
+1
-1
public/app/features/alerting/notification_edit_ctrl.ts
+1
-1
public/app/features/alerting/partials/notification_edit.html
+11
-6
No files found.
appveyor.yml
View file @
a826591e
...
@@ -25,10 +25,11 @@ install:
...
@@ -25,10 +25,11 @@ install:
build_script
:
build_script
:
-
go run build.go build
-
go run build.go build
-
grunt release
-
grunt release
-
go run build.go sha1-dist
-
cp dist/* .
-
cp dist/* .
artifacts
:
artifacts
:
-
path
:
grafana-*windows-*.
zip
-
path
:
grafana-*windows-*.
*
name
:
binzip
name
:
binzip
deploy
:
deploy
:
...
...
build.go
View file @
a826591e
...
@@ -98,6 +98,9 @@ func main() {
...
@@ -98,6 +98,9 @@ func main() {
createDebPackages
()
createDebPackages
()
sha1FilesInDist
()
sha1FilesInDist
()
case
"sha1-dist"
:
sha1FilesInDist
()
case
"latest"
:
case
"latest"
:
makeLatestDistCopies
()
makeLatestDistCopies
()
sha1FilesInDist
()
sha1FilesInDist
()
...
...
docker/blocks/influxdb/config.toml
deleted
100644 → 0
View file @
a8dd4491
bind-address
=
"0.0.0.0"
[logging]
level
=
"debug"
file
=
"/opt/influxdb/shared/data/influxdb.log"
# stdout to log to standard out
[admin]
port
=
8083
# binding is disabled if the port isn't set
assets
=
"/opt/influxdb/current/admin"
[api]
port
=
8086
# binding is disabled if the port isn't set
read-timeout
=
"5s"
[input_plugins]
[input_plugins.graphite]
enabled
=
true
port
=
2004
database
=
"graphite"
# store graphite data in this database
[raft]
port
=
8090
dir
=
"/opt/influxdb/shared/data/raft"
[storage]
dir
=
"/opt/influxdb/shared/data/db"
# How many requests to potentially buffer in memory. If the buffer gets filled then writes
# will still be logged and once the local storage has caught up (or compacted) the writes
# will be replayed from the WAL
write-buffer-size
=
10000
default-engine
=
"rocksdb"
max-open-shards
=
0
point-batch-size
=
100
write-batch-size
=
5000000
retention-sweep-period
=
"10m"
[storage.engines.rocksdb]
max-open-files
=
1000
lru-cache-size
=
"200m"
[storage.engines.leveldb]
max-open-files
=
1000
lru-cache-size
=
"200m"
[cluster]
protobuf_port
=
8099
protobuf_timeout
=
"2s"
# the write timeout on the protobuf conn any duration parseable by time.ParseDuration
protobuf_heartbeat
=
"200ms"
# the heartbeat interval between the servers. must be parseable by time.ParseDuration
protobuf_min_backoff
=
"1s"
# the minimum backoff after a failed heartbeat attempt
protobuf_max_backoff
=
"10s"
# the maxmimum backoff after a failed heartbeat attempt
write-buffer-size
=
10000
ax-response-buffer-size
=
100000
oncurrent-shard-query-limit
=
10
[sharding]
replication-factor
=
1
[sharding.short-term]
duration
=
"7d"
split
=
1
[sharding.long-term]
duration
=
"30d"
split
=
1
# split-random = "/^Hf.*/"
[wal]
dir
=
"/opt/influxdb/shared/data/wal"
flush-after
=
1000
# the number of writes after which wal will be flushed, 0 for flushing on every write
bookmark-after
=
1000
# the number of writes after which a bookmark will be created
index-after
=
1000
requests-per-logfile
=
10000
docker/blocks/influxdb/fig
View file @
a826591e
influxdb:
influxdb:
#image: influxdb/influxdb:1.0-alpine
image: influxdb:latest
image: influxdb:latest
container_name: influxdb
container_name: influxdb
ports:
ports:
- "2004:2004"
- "2004:2004"
- "8083:8083"
- "8083:8083"
- "8086:8086"
- "8086:8086"
volumes:
- ./blocks/influxdb/influxdb.conf:/etc/influxdb/influxdb.conf
fake-influxdb-data:
fake-influxdb-data:
image: grafana/fake-data-gen
image: grafana/fake-data-gen
...
...
docker/blocks/influxdb/influxdb.conf
0 → 100644
View file @
a826591e
reporting
-
disabled
=
false
[
meta
]
# Where the metadata/raft database is stored
dir
=
"/var/lib/influxdb/meta"
retention
-
autocreate
=
true
# If log messages are printed for the meta service
logging
-
enabled
=
true
pprof
-
enabled
=
false
# The default duration for leases.
lease
-
duration
=
"1m0s"
[
data
]
# Controls if this node holds time series data shards in the cluster
enabled
=
true
dir
=
"/var/lib/influxdb/data"
# These are the WAL settings for the storage engine >= 0.9.3
wal
-
dir
=
"/var/lib/influxdb/wal"
wal
-
logging
-
enabled
=
true
[
coordinator
]
write
-
timeout
=
"10s"
max
-
concurrent
-
queries
=
0
query
-
timeout
=
"0"
log
-
queries
-
after
=
"0"
max
-
select
-
point
=
0
max
-
select
-
series
=
0
max
-
select
-
buckets
=
0
[
retention
]
enabled
=
true
check
-
interval
=
"30m"
[
shard
-
precreation
]
enabled
=
true
check
-
interval
=
"10m"
advance
-
period
=
"30m"
[
monitor
]
store
-
enabled
=
true
# Whether to record statistics internally.
store
-
database
=
"_internal"
# The destination database for recorded statistics
store
-
interval
=
"10s"
# The interval at which to record statistics
[
admin
]
enabled
=
true
bind
-
address
=
":8083"
https
-
enabled
=
false
https
-
certificate
=
"/etc/ssl/influxdb.pem"
[
http
]
enabled
=
true
bind
-
address
=
":8086"
auth
-
enabled
=
true
log
-
enabled
=
true
write
-
tracing
=
false
pprof
-
enabled
=
false
https
-
enabled
=
false
https
-
certificate
=
"/etc/ssl/influxdb.pem"
### Use a separate private key location.
# https-private-key = ""
max
-
row
-
limit
=
10000
realm
=
"InfluxDB"
unix
-
socket
-
enabled
=
false
# enable http service over unix domain socket
# bind-socket = "/var/run/influxdb.sock"
[
subscriber
]
enabled
=
true
[[
graphite
]]
enabled
=
false
[[
collectd
]]
enabled
=
false
[[
opentsdb
]]
enabled
=
false
[[
udp
]]
enabled
=
false
[
continuous_queries
]
log
-
enabled
=
true
enabled
=
true
# run-interval = "1s" # interval for how often continuous queries will be checked if they need to run
pkg/api/api.go
View file @
a826591e
...
@@ -252,7 +252,7 @@ func Register(r *macaron.Macaron) {
...
@@ -252,7 +252,7 @@ func Register(r *macaron.Macaron) {
r
.
Group
(
"/alerts"
,
func
()
{
r
.
Group
(
"/alerts"
,
func
()
{
r
.
Post
(
"/test"
,
bind
(
dtos
.
AlertTestCommand
{}),
wrap
(
AlertTest
))
r
.
Post
(
"/test"
,
bind
(
dtos
.
AlertTestCommand
{}),
wrap
(
AlertTest
))
r
.
Post
(
"/:alertId/pause"
,
bind
(
dtos
.
PauseAlertCommand
{}),
wrap
(
PauseAlert
))
r
.
Post
(
"/:alertId/pause"
,
bind
(
dtos
.
PauseAlertCommand
{}),
wrap
(
PauseAlert
)
,
reqEditorRole
)
r
.
Get
(
"/:alertId"
,
ValidateOrgAlert
,
wrap
(
GetAlert
))
r
.
Get
(
"/:alertId"
,
ValidateOrgAlert
,
wrap
(
GetAlert
))
r
.
Get
(
"/"
,
wrap
(
GetAlerts
))
r
.
Get
(
"/"
,
wrap
(
GetAlerts
))
r
.
Get
(
"/states-for-dashboard"
,
wrap
(
GetAlertStatesForDashboard
))
r
.
Get
(
"/states-for-dashboard"
,
wrap
(
GetAlertStatesForDashboard
))
...
@@ -266,7 +266,7 @@ func Register(r *macaron.Macaron) {
...
@@ -266,7 +266,7 @@ func Register(r *macaron.Macaron) {
r
.
Put
(
"/:notificationId"
,
bind
(
m
.
UpdateAlertNotificationCommand
{}),
wrap
(
UpdateAlertNotification
))
r
.
Put
(
"/:notificationId"
,
bind
(
m
.
UpdateAlertNotificationCommand
{}),
wrap
(
UpdateAlertNotification
))
r
.
Get
(
"/:notificationId"
,
wrap
(
GetAlertNotificationById
))
r
.
Get
(
"/:notificationId"
,
wrap
(
GetAlertNotificationById
))
r
.
Delete
(
"/:notificationId"
,
wrap
(
DeleteAlertNotification
))
r
.
Delete
(
"/:notificationId"
,
wrap
(
DeleteAlertNotification
))
},
req
OrgAdmin
)
},
req
EditorRole
)
r
.
Get
(
"/annotations"
,
wrap
(
GetAnnotations
))
r
.
Get
(
"/annotations"
,
wrap
(
GetAnnotations
))
r
.
Post
(
"/annotations/mass-delete"
,
reqOrgAdmin
,
bind
(
dtos
.
DeleteAnnotationsCmd
{}),
wrap
(
DeleteAnnotations
))
r
.
Post
(
"/annotations/mass-delete"
,
reqOrgAdmin
,
bind
(
dtos
.
DeleteAnnotationsCmd
{}),
wrap
(
DeleteAnnotations
))
...
...
pkg/models/notifications.go
View file @
a826591e
...
@@ -21,6 +21,7 @@ type SendWebhook struct {
...
@@ -21,6 +21,7 @@ type SendWebhook struct {
User
string
User
string
Password
string
Password
string
Body
string
Body
string
HttpMethod
string
}
}
type
SendWebhookSync
struct
{
type
SendWebhookSync
struct
{
...
@@ -28,6 +29,7 @@ type SendWebhookSync struct {
...
@@ -28,6 +29,7 @@ type SendWebhookSync struct {
User
string
User
string
Password
string
Password
string
Body
string
Body
string
HttpMethod
string
}
}
type
SendResetPasswordEmailCommand
struct
{
type
SendResetPasswordEmailCommand
struct
{
...
...
pkg/services/alerting/notifiers/webhook.go
View file @
a826591e
...
@@ -24,6 +24,7 @@ func NewWebHookNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
...
@@ -24,6 +24,7 @@ func NewWebHookNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
Url
:
url
,
Url
:
url
,
User
:
model
.
Settings
.
Get
(
"user"
)
.
MustString
(),
User
:
model
.
Settings
.
Get
(
"user"
)
.
MustString
(),
Password
:
model
.
Settings
.
Get
(
"password"
)
.
MustString
(),
Password
:
model
.
Settings
.
Get
(
"password"
)
.
MustString
(),
HttpMethod
:
model
.
Settings
.
Get
(
"httpMethod"
)
.
MustString
(
"POST"
),
log
:
log
.
New
(
"alerting.notifier.webhook"
),
log
:
log
.
New
(
"alerting.notifier.webhook"
),
},
nil
},
nil
}
}
...
@@ -33,6 +34,7 @@ type WebhookNotifier struct {
...
@@ -33,6 +34,7 @@ type WebhookNotifier struct {
Url
string
Url
string
User
string
User
string
Password
string
Password
string
HttpMethod
string
log
log
.
Logger
log
log
.
Logger
}
}
...
@@ -63,6 +65,7 @@ func (this *WebhookNotifier) Notify(evalContext *alerting.EvalContext) error {
...
@@ -63,6 +65,7 @@ func (this *WebhookNotifier) Notify(evalContext *alerting.EvalContext) error {
User
:
this
.
User
,
User
:
this
.
User
,
Password
:
this
.
Password
,
Password
:
this
.
Password
,
Body
:
string
(
body
),
Body
:
string
(
body
),
HttpMethod
:
this
.
HttpMethod
,
}
}
if
err
:=
bus
.
DispatchCtx
(
evalContext
.
Ctx
,
cmd
);
err
!=
nil
{
if
err
:=
bus
.
DispatchCtx
(
evalContext
.
Ctx
,
cmd
);
err
!=
nil
{
...
...
pkg/services/notifications/notifications.go
View file @
a826591e
...
@@ -65,6 +65,7 @@ func SendWebhookSync(ctx context.Context, cmd *m.SendWebhookSync) error {
...
@@ -65,6 +65,7 @@ func SendWebhookSync(ctx context.Context, cmd *m.SendWebhookSync) error {
User
:
cmd
.
User
,
User
:
cmd
.
User
,
Password
:
cmd
.
Password
,
Password
:
cmd
.
Password
,
Body
:
cmd
.
Body
,
Body
:
cmd
.
Body
,
HttpMethod
:
cmd
.
HttpMethod
,
})
})
}
}
...
@@ -74,6 +75,7 @@ func sendWebhook(cmd *m.SendWebhook) error {
...
@@ -74,6 +75,7 @@ func sendWebhook(cmd *m.SendWebhook) error {
User
:
cmd
.
User
,
User
:
cmd
.
User
,
Password
:
cmd
.
Password
,
Password
:
cmd
.
Password
,
Body
:
cmd
.
Body
,
Body
:
cmd
.
Body
,
HttpMethod
:
cmd
.
HttpMethod
,
})
})
return
nil
return
nil
...
...
pkg/services/notifications/webhook.go
View file @
a826591e
...
@@ -19,6 +19,7 @@ type Webhook struct {
...
@@ -19,6 +19,7 @@ type Webhook struct {
User
string
User
string
Password
string
Password
string
Body
string
Body
string
HttpMethod
string
}
}
var
webhookQueue
chan
*
Webhook
var
webhookQueue
chan
*
Webhook
...
@@ -44,13 +45,17 @@ func processWebhookQueue() {
...
@@ -44,13 +45,17 @@ func processWebhookQueue() {
}
}
func
sendWebRequestSync
(
ctx
context
.
Context
,
webhook
*
Webhook
)
error
{
func
sendWebRequestSync
(
ctx
context
.
Context
,
webhook
*
Webhook
)
error
{
webhookLog
.
Debug
(
"Sending webhook"
,
"url"
,
webhook
.
Url
)
webhookLog
.
Debug
(
"Sending webhook"
,
"url"
,
webhook
.
Url
,
"http method"
,
webhook
.
HttpMethod
)
client
:=
&
http
.
Client
{
client
:=
&
http
.
Client
{
Timeout
:
time
.
Duration
(
10
*
time
.
Second
),
Timeout
:
time
.
Duration
(
10
*
time
.
Second
),
}
}
request
,
err
:=
http
.
NewRequest
(
http
.
MethodPost
,
webhook
.
Url
,
bytes
.
NewReader
([]
byte
(
webhook
.
Body
)))
if
webhook
.
HttpMethod
==
""
{
webhook
.
HttpMethod
=
http
.
MethodPost
}
request
,
err
:=
http
.
NewRequest
(
webhook
.
HttpMethod
,
webhook
.
Url
,
bytes
.
NewReader
([]
byte
(
webhook
.
Body
)))
if
webhook
.
User
!=
""
&&
webhook
.
Password
!=
""
{
if
webhook
.
User
!=
""
&&
webhook
.
Password
!=
""
{
request
.
Header
.
Add
(
"Authorization"
,
util
.
GetBasicAuthHeader
(
webhook
.
User
,
webhook
.
Password
))
request
.
Header
.
Add
(
"Authorization"
,
util
.
GetBasicAuthHeader
(
webhook
.
User
,
webhook
.
Password
))
}
}
...
...
pkg/social/google_oauth.go
View file @
a826591e
...
@@ -46,5 +46,6 @@ func (s *SocialGoogle) UserInfo(client *http.Client) (*BasicUserInfo, error) {
...
@@ -46,5 +46,6 @@ func (s *SocialGoogle) UserInfo(client *http.Client) (*BasicUserInfo, error) {
return
&
BasicUserInfo
{
return
&
BasicUserInfo
{
Name
:
data
.
Name
,
Name
:
data
.
Name
,
Email
:
data
.
Email
,
Email
:
data
.
Email
,
Login
:
data
.
Email
,
},
nil
},
nil
}
}
pkg/tsdb/influxdb/influxdb.go
View file @
a826591e
...
@@ -124,10 +124,15 @@ func (e *InfluxDBExecutor) createRequest(query string) (*http.Request, error) {
...
@@ -124,10 +124,15 @@ func (e *InfluxDBExecutor) createRequest(query string) (*http.Request, error) {
req
.
URL
.
RawQuery
=
params
.
Encode
()
req
.
URL
.
RawQuery
=
params
.
Encode
()
req
.
Header
.
Set
(
"User-Agent"
,
"Grafana"
)
req
.
Header
.
Set
(
"User-Agent"
,
"Grafana"
)
if
e
.
BasicAuth
{
if
e
.
BasicAuth
{
req
.
SetBasicAuth
(
e
.
BasicAuthUser
,
e
.
BasicAuthPassword
)
req
.
SetBasicAuth
(
e
.
BasicAuthUser
,
e
.
BasicAuthPassword
)
}
}
if
e
.
User
!=
""
{
req
.
SetBasicAuth
(
e
.
User
,
e
.
Password
)
}
glog
.
Debug
(
"Influxdb request"
,
"url"
,
req
.
URL
.
String
())
glog
.
Debug
(
"Influxdb request"
,
"url"
,
req
.
URL
.
String
())
return
req
,
nil
return
req
,
nil
}
}
pkg/tsdb/prometheus/prometheus.go
View file @
a826591e
...
@@ -84,8 +84,10 @@ func formatLegend(metric pmodel.Metric, query *PrometheusQuery) string {
...
@@ -84,8 +84,10 @@ func formatLegend(metric pmodel.Metric, query *PrometheusQuery) string {
reg
,
_
:=
regexp
.
Compile
(
`\{\{\s*(.+?)\s*\}\}`
)
reg
,
_
:=
regexp
.
Compile
(
`\{\{\s*(.+?)\s*\}\}`
)
result
:=
reg
.
ReplaceAllFunc
([]
byte
(
query
.
LegendFormat
),
func
(
in
[]
byte
)
[]
byte
{
result
:=
reg
.
ReplaceAllFunc
([]
byte
(
query
.
LegendFormat
),
func
(
in
[]
byte
)
[]
byte
{
ind
:=
strings
.
Replace
(
strings
.
Replace
(
string
(
in
),
"{{"
,
""
,
1
),
"}}"
,
""
,
1
)
labelName
:=
strings
.
Replace
(
string
(
in
),
"{{"
,
""
,
1
)
if
val
,
exists
:=
metric
[
pmodel
.
LabelName
(
ind
)];
exists
{
labelName
=
strings
.
Replace
(
labelName
,
"}}"
,
""
,
1
)
labelName
=
strings
.
TrimSpace
(
labelName
)
if
val
,
exists
:=
metric
[
pmodel
.
LabelName
(
labelName
)];
exists
{
return
[]
byte
(
val
)
return
[]
byte
(
val
)
}
}
...
...
pkg/tsdb/prometheus/prometheus_test.go
View file @
a826591e
...
@@ -17,7 +17,7 @@ func TestPrometheus(t *testing.T) {
...
@@ -17,7 +17,7 @@ func TestPrometheus(t *testing.T) {
}
}
query
:=
&
PrometheusQuery
{
query
:=
&
PrometheusQuery
{
LegendFormat
:
"legend {{app}} {{
device
}} {{broken}}"
,
LegendFormat
:
"legend {{app}} {{
device
}} {{broken}}"
,
}
}
So
(
formatLegend
(
metric
,
query
),
ShouldEqual
,
"legend backend mobile {{broken}}"
)
So
(
formatLegend
(
metric
,
query
),
ShouldEqual
,
"legend backend mobile {{broken}}"
)
...
...
public/app/features/alerting/notification_edit_ctrl.ts
View file @
a826591e
...
@@ -18,7 +18,7 @@ export class AlertNotificationEditCtrl {
...
@@ -18,7 +18,7 @@ export class AlertNotificationEditCtrl {
this
.
model
=
{
this
.
model
=
{
type
:
'email'
,
type
:
'email'
,
settings
:
{
settings
:
{
severityFilter
:
'none
'
httpMethod
:
'POST
'
},
},
isDefault
:
false
isDefault
:
false
};
};
...
...
public/app/features/alerting/partials/notification_edit.html
View file @
a826591e
...
@@ -32,18 +32,23 @@
...
@@ -32,18 +32,23 @@
<div
class=
"gf-form-group"
ng-if=
"ctrl.model.type === 'webhook'"
>
<div
class=
"gf-form-group"
ng-if=
"ctrl.model.type === 'webhook'"
>
<h3
class=
"page-heading"
>
Webhook settings
</h3>
<h3
class=
"page-heading"
>
Webhook settings
</h3>
<div
class=
"gf-form"
>
<div
class=
"gf-form"
>
<span
class=
"gf-form-label width-
6
"
>
Url
</span>
<span
class=
"gf-form-label width-
10
"
>
Url
</span>
<input
type=
"text"
required
class=
"gf-form-input max-width-26"
ng-model=
"ctrl.model.settings.url"
></input>
<input
type=
"text"
required
class=
"gf-form-input max-width-26"
ng-model=
"ctrl.model.settings.url"
></input>
</div>
</div>
<div
class=
"gf-form-inline"
>
<div
class=
"gf-form"
>
<div
class=
"gf-form"
>
<span
class=
"gf-form-label width-6"
>
Username
</span>
<span
class=
"gf-form-label width-10"
>
Http Method
</span>
<input
type=
"text"
class=
"gf-form-input max-width-10"
ng-model=
"ctrl.model.settings.username"
></input>
<div
class=
"gf-form-select-wrapper width-14"
>
<select
class=
"gf-form-input"
ng-model=
"ctrl.model.settings.httpMethod"
ng-options=
"t for t in ['POST', 'PUT']"
>
</select>
</div>
</div>
</div>
<div
class=
"gf-form"
>
<div
class=
"gf-form"
>
<span
class=
"gf-form-label width-6"
>
Password
</span>
<span
class=
"gf-form-label width-10"
>
Username
</span>
<input
type=
"text"
class=
"gf-form-input max-width-10"
ng-model=
"ctrl.model.settings.password
"
></input>
<input
type=
"text"
class=
"gf-form-input max-width-14"
ng-model=
"ctrl.model.settings.username
"
></input>
</div>
</div>
<div
class=
"gf-form"
>
<span
class=
"gf-form-label width-10"
>
Password
</span>
<input
type=
"text"
class=
"gf-form-input max-width-14"
ng-model=
"ctrl.model.settings.password"
></input>
</div>
</div>
</div>
</div>
...
...
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