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
a79bd424
Unverified
Commit
a79bd424
authored
Feb 11, 2019
by
Torkel Ödegaard
Committed by
GitHub
Feb 11, 2019
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #15306 from grafana/explore/dedup-strategu-url
Persist deduplication strategy in url
parents
951e5932
85780eb3
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
121 additions
and
39 deletions
+121
-39
public/app/core/utils/explore.test.ts
+4
-2
public/app/core/utils/explore.ts
+4
-1
public/app/features/explore/GraphContainer.tsx
+1
-1
public/app/features/explore/Logs.tsx
+14
-13
public/app/features/explore/LogsContainer.tsx
+26
-4
public/app/features/explore/TableContainer.tsx
+1
-1
public/app/features/explore/state/actionTypes.ts
+9
-0
public/app/features/explore/state/actions.ts
+41
-12
public/app/features/explore/state/reducers.ts
+11
-4
public/app/features/panel/specs/metrics_panel_ctrl.test.ts
+3
-0
public/app/types/explore.ts
+7
-1
No files found.
public/app/core/utils/explore.test.ts
View file @
a79bd424
...
...
@@ -8,6 +8,7 @@ import {
}
from
'./explore'
;
import
{
ExploreUrlState
}
from
'app/types/explore'
;
import
store
from
'app/core/store'
;
import
{
LogsDedupStrategy
}
from
'app/core/logs_model'
;
const
DEFAULT_EXPLORE_STATE
:
ExploreUrlState
=
{
datasource
:
null
,
...
...
@@ -17,6 +18,7 @@ const DEFAULT_EXPLORE_STATE: ExploreUrlState = {
showingGraph
:
true
,
showingTable
:
true
,
showingLogs
:
true
,
dedupStrategy
:
LogsDedupStrategy
.
none
,
}
};
...
...
@@ -78,7 +80,7 @@ describe('state functions', () => {
expect
(
serializeStateToUrlParam
(
state
)).
toBe
(
'{"datasource":"foo","queries":[{"expr":"metric{test=
\\
"a/b
\\
"}"},'
+
'{"expr":"super{foo=
\\
"x/z
\\
"}"}],"range":{"from":"now-5h","to":"now"},'
+
'"ui":{"showingGraph":true,"showingTable":true,"showingLogs":true}}'
'"ui":{"showingGraph":true,"showingTable":true,"showingLogs":true
,"dedupStrategy":"none"
}}'
);
});
...
...
@@ -100,7 +102,7 @@ describe('state functions', () => {
},
};
expect
(
serializeStateToUrlParam
(
state
,
true
)).
toBe
(
'["now-5h","now","foo",{"expr":"metric{test=
\\
"a/b
\\
"}"},{"expr":"super{foo=
\\
"x/z
\\
"}"},{"ui":[true,true,true]}]'
'["now-5h","now","foo",{"expr":"metric{test=
\\
"a/b
\\
"}"},{"expr":"super{foo=
\\
"x/z
\\
"}"},{"ui":[true,true,true
,"none"
]}]'
);
});
});
...
...
public/app/core/utils/explore.ts
View file @
a79bd424
...
...
@@ -21,6 +21,7 @@ import {
QueryIntervals
,
QueryOptions
,
}
from
'app/types/explore'
;
import
{
LogsDedupStrategy
}
from
'app/core/logs_model'
;
export
const
DEFAULT_RANGE
=
{
from
:
'now-6h'
,
...
...
@@ -31,6 +32,7 @@ export const DEFAULT_UI_STATE = {
showingTable
:
true
,
showingGraph
:
true
,
showingLogs
:
true
,
dedupStrategy
:
LogsDedupStrategy
.
none
,
};
const
MAX_HISTORY_ITEMS
=
100
;
...
...
@@ -183,6 +185,7 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
showingGraph
:
segment
.
ui
[
0
],
showingLogs
:
segment
.
ui
[
1
],
showingTable
:
segment
.
ui
[
2
],
dedupStrategy
:
segment
.
ui
[
3
],
};
}
});
...
...
@@ -204,7 +207,7 @@ export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: bo
urlState
.
range
.
to
,
urlState
.
datasource
,
...
urlState
.
queries
,
{
ui
:
[
!!
urlState
.
ui
.
showingGraph
,
!!
urlState
.
ui
.
showingLogs
,
!!
urlState
.
ui
.
showingTable
]
},
{
ui
:
[
!!
urlState
.
ui
.
showingGraph
,
!!
urlState
.
ui
.
showingLogs
,
!!
urlState
.
ui
.
showingTable
,
urlState
.
ui
.
dedupStrategy
]
},
]);
}
return
JSON
.
stringify
(
urlState
);
...
...
public/app/features/explore/GraphContainer.tsx
View file @
a79bd424
...
...
@@ -25,7 +25,7 @@ interface GraphContainerProps {
export
class
GraphContainer
extends
PureComponent
<
GraphContainerProps
>
{
onClickGraphButton
=
()
=>
{
this
.
props
.
toggleGraph
(
this
.
props
.
exploreId
);
this
.
props
.
toggleGraph
(
this
.
props
.
exploreId
,
this
.
props
.
showingGraph
);
};
onChangeTime
=
(
timeRange
:
TimeRange
)
=>
{
...
...
public/app/features/explore/Logs.tsx
View file @
a79bd424
...
...
@@ -58,14 +58,15 @@ interface Props {
range
?:
RawTimeRange
;
scanning
?:
boolean
;
scanRange
?:
RawTimeRange
;
dedupStrategy
:
LogsDedupStrategy
;
onChangeTime
?:
(
range
:
RawTimeRange
)
=>
void
;
onClickLabel
?:
(
label
:
string
,
value
:
string
)
=>
void
;
onStartScanning
?:
()
=>
void
;
onStopScanning
?:
()
=>
void
;
onDedupStrategyChange
:
(
dedupStrategy
:
LogsDedupStrategy
)
=>
void
;
}
interface
State
{
dedup
:
LogsDedupStrategy
;
deferLogs
:
boolean
;
hiddenLogLevels
:
Set
<
LogLevel
>
;
renderAll
:
boolean
;
...
...
@@ -79,7 +80,6 @@ export default class Logs extends PureComponent<Props, State> {
renderAllTimer
:
NodeJS
.
Timer
;
state
=
{
dedup
:
LogsDedupStrategy
.
none
,
deferLogs
:
true
,
hiddenLogLevels
:
new
Set
(),
renderAll
:
false
,
...
...
@@ -112,12 +112,11 @@ export default class Logs extends PureComponent<Props, State> {
}
onChangeDedup
=
(
dedup
:
LogsDedupStrategy
)
=>
{
this
.
setState
(
prevState
=>
{
if
(
prevState
.
dedup
===
dedup
)
{
return
{
dedup
:
LogsDedupStrategy
.
none
};
}
return
{
dedup
};
});
const
{
onDedupStrategyChange
}
=
this
.
props
;
if
(
this
.
props
.
dedupStrategy
===
dedup
)
{
return
onDedupStrategyChange
(
LogsDedupStrategy
.
none
);
}
return
onDedupStrategyChange
(
dedup
);
};
onChangeLabels
=
(
event
:
React
.
SyntheticEvent
)
=>
{
...
...
@@ -173,17 +172,19 @@ export default class Logs extends PureComponent<Props, State> {
return
null
;
}
const
{
de
dup
,
deferLogs
,
hiddenLogLevels
,
renderAll
,
showLocalTime
,
showUtc
}
=
this
.
state
;
const
{
de
ferLogs
,
hiddenLogLevels
,
renderAll
,
showLocalTime
,
showUtc
,
}
=
this
.
state
;
let
{
showLabels
}
=
this
.
state
;
const
{
dedupStrategy
}
=
this
.
props
;
const
hasData
=
data
&&
data
.
rows
&&
data
.
rows
.
length
>
0
;
const
showDuplicates
=
dedup
!==
LogsDedupStrategy
.
none
;
const
showDuplicates
=
dedup
Strategy
!==
LogsDedupStrategy
.
none
;
// Filtering
const
filteredData
=
filterLogLevels
(
data
,
hiddenLogLevels
);
const
dedupedData
=
dedupLogRows
(
filteredData
,
dedup
);
const
dedupedData
=
dedupLogRows
(
filteredData
,
dedup
Strategy
);
const
dedupCount
=
dedupedData
.
rows
.
reduce
((
sum
,
row
)
=>
sum
+
row
.
duplicates
,
0
);
const
meta
=
[...
data
.
meta
];
if
(
dedup
!==
LogsDedupStrategy
.
none
)
{
if
(
dedupStrategy
!==
LogsDedupStrategy
.
none
)
{
meta
.
push
({
label
:
'Dedup count'
,
value
:
dedupCount
,
...
...
@@ -236,7 +237,7 @@ export default class Logs extends PureComponent<Props, State> {
key=
{
i
}
value=
{
dedupType
}
onChange=
{
this
.
onChangeDedup
}
selected=
{
dedup
===
dedupType
}
selected=
{
dedup
Strategy
===
dedupType
}
tooltip=
{
LogsDedupDescription
[
dedupType
]
}
>
{
dedupType
}
...
...
public/app/features/explore/LogsContainer.tsx
View file @
a79bd424
...
...
@@ -4,10 +4,10 @@ import { connect } from 'react-redux';
import
{
RawTimeRange
,
TimeRange
}
from
'@grafana/ui'
;
import
{
ExploreId
,
ExploreItemState
}
from
'app/types/explore'
;
import
{
LogsModel
}
from
'app/core/logs_model'
;
import
{
LogsModel
,
LogsDedupStrategy
}
from
'app/core/logs_model'
;
import
{
StoreState
}
from
'app/types'
;
import
{
toggleLogs
}
from
'./state/actions'
;
import
{
toggleLogs
,
changeDedupStrategy
}
from
'./state/actions'
;
import
Logs
from
'./Logs'
;
import
Panel
from
'./Panel'
;
...
...
@@ -25,12 +25,18 @@ interface LogsContainerProps {
scanRange
?:
RawTimeRange
;
showingLogs
:
boolean
;
toggleLogs
:
typeof
toggleLogs
;
changeDedupStrategy
:
typeof
changeDedupStrategy
;
dedupStrategy
:
LogsDedupStrategy
;
width
:
number
;
}
export
class
LogsContainer
extends
PureComponent
<
LogsContainerProps
>
{
onClickLogsButton
=
()
=>
{
this
.
props
.
toggleLogs
(
this
.
props
.
exploreId
);
this
.
props
.
toggleLogs
(
this
.
props
.
exploreId
,
this
.
props
.
showingLogs
);
};
handleDedupStrategyChange
=
(
dedupStrategy
:
LogsDedupStrategy
)
=>
{
this
.
props
.
changeDedupStrategy
(
this
.
props
.
exploreId
,
dedupStrategy
);
};
render
()
{
...
...
@@ -53,6 +59,7 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
return
(
<
Panel
label=
"Logs"
loading=
{
loading
}
isOpen=
{
showingLogs
}
onToggle=
{
this
.
onClickLogsButton
}
>
<
Logs
dedupStrategy=
{
this
.
props
.
dedupStrategy
||
LogsDedupStrategy
.
none
}
data=
{
logsResult
}
exploreId=
{
exploreId
}
key=
{
logsResult
&&
logsResult
.
id
}
...
...
@@ -62,6 +69,7 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
onClickLabel=
{
onClickLabel
}
onStartScanning=
{
onStartScanning
}
onStopScanning=
{
onStopScanning
}
onDedupStrategyChange=
{
this
.
handleDedupStrategyChange
}
range=
{
range
}
scanning=
{
scanning
}
scanRange=
{
scanRange
}
...
...
@@ -72,11 +80,23 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
}
}
const
selectItemUIState
=
(
itemState
:
ExploreItemState
)
=>
{
const
{
showingGraph
,
showingLogs
,
showingTable
,
showingStartPage
,
dedupStrategy
}
=
itemState
;
return
{
showingGraph
,
showingLogs
,
showingTable
,
showingStartPage
,
dedupStrategy
,
};
};
function
mapStateToProps
(
state
:
StoreState
,
{
exploreId
})
{
const
explore
=
state
.
explore
;
const
item
:
ExploreItemState
=
explore
[
exploreId
];
const
{
logsHighlighterExpressions
,
logsResult
,
queryTransactions
,
scanning
,
scanRange
,
showingLogs
,
range
}
=
item
;
const
{
logsHighlighterExpressions
,
logsResult
,
queryTransactions
,
scanning
,
scanRange
,
range
}
=
item
;
const
loading
=
queryTransactions
.
some
(
qt
=>
qt
.
resultType
===
'Logs'
&&
!
qt
.
done
);
const
{
showingLogs
,
dedupStrategy
}
=
selectItemUIState
(
item
);
return
{
loading
,
logsHighlighterExpressions
,
...
...
@@ -85,11 +105,13 @@ function mapStateToProps(state: StoreState, { exploreId }) {
scanRange
,
showingLogs
,
range
,
dedupStrategy
,
};
}
const
mapDispatchToProps
=
{
toggleLogs
,
changeDedupStrategy
,
};
export
default
hot
(
module
)(
connect
(
mapStateToProps
,
mapDispatchToProps
)(
LogsContainer
));
public/app/features/explore/TableContainer.tsx
View file @
a79bd424
...
...
@@ -21,7 +21,7 @@ interface TableContainerProps {
export
class
TableContainer
extends
PureComponent
<
TableContainerProps
>
{
onClickTableButton
=
()
=>
{
this
.
props
.
toggleTable
(
this
.
props
.
exploreId
);
this
.
props
.
toggleTable
(
this
.
props
.
exploreId
,
this
.
props
.
showingTable
);
};
render
()
{
...
...
public/app/features/explore/state/actionTypes.ts
View file @
a79bd424
...
...
@@ -192,6 +192,10 @@ export interface ToggleLogsPayload {
exploreId
:
ExploreId
;
}
export
interface
UpdateUIStatePayload
extends
Partial
<
ExploreUIState
>
{
exploreId
:
ExploreId
;
}
export
interface
UpdateDatasourceInstancePayload
{
exploreId
:
ExploreId
;
datasourceInstance
:
DataSourceApi
;
...
...
@@ -367,6 +371,11 @@ export const splitOpenAction = actionCreatorFactory<SplitOpenPayload>('explore/S
export
const
stateSaveAction
=
noPayloadActionCreatorFactory
(
'explore/STATE_SAVE'
).
create
();
/**
* Update state of Explores UI elements (panels visiblity and deduplication strategy)
*/
export
const
updateUIStateAction
=
actionCreatorFactory
<
UpdateUIStatePayload
>
(
'explore/UPDATE_UI_STATE'
).
create
();
/**
* Expand/collapse the table result viewer. When collapsed, table queries won't be run.
*/
export
const
toggleTableAction
=
actionCreatorFactory
<
ToggleTablePayload
>
(
'explore/TOGGLE_TABLE'
).
create
();
...
...
public/app/features/explore/state/actions.ts
View file @
a79bd424
...
...
@@ -67,14 +67,26 @@ import {
ToggleGraphPayload
,
ToggleLogsPayload
,
ToggleTablePayload
,
updateUIStateAction
,
}
from
'./actionTypes'
;
import
{
ActionOf
,
ActionCreator
}
from
'app/core/redux/actionCreatorFactory'
;
import
{
LogsDedupStrategy
}
from
'app/core/logs_model'
;
type
ThunkResult
<
R
>
=
ThunkAction
<
R
,
StoreState
,
undefined
,
Action
>
;
// /**
// * Adds a query row after the row with the given index.
// */
/**
* Updates UI state and save it to the URL
*/
const
updateExploreUIState
=
(
exploreId
,
uiStateFragment
:
Partial
<
ExploreUIState
>
)
=>
{
return
dispatch
=>
{
dispatch
(
updateUIStateAction
({
exploreId
,
...
uiStateFragment
}));
dispatch
(
stateSave
());
};
};
/**
* Adds a query row after the row with the given index.
*/
export
function
addQueryRow
(
exploreId
:
ExploreId
,
index
:
number
):
ActionOf
<
AddQueryRowPayload
>
{
const
query
=
generateEmptyQuery
(
index
+
1
);
return
addQueryRowAction
({
exploreId
,
index
,
query
});
...
...
@@ -669,6 +681,7 @@ export function stateSave() {
showingGraph
:
left
.
showingGraph
,
showingLogs
:
left
.
showingLogs
,
showingTable
:
left
.
showingTable
,
dedupStrategy
:
left
.
dedupStrategy
,
},
};
urlStates
.
left
=
serializeStateToUrlParam
(
leftUrlState
,
true
);
...
...
@@ -677,7 +690,12 @@ export function stateSave() {
datasource
:
right
.
datasourceInstance
.
name
,
queries
:
right
.
queries
.
map
(
clearQueryKeys
),
range
:
right
.
range
,
ui
:
{
showingGraph
:
right
.
showingGraph
,
showingLogs
:
right
.
showingLogs
,
showingTable
:
right
.
showingTable
},
ui
:
{
showingGraph
:
right
.
showingGraph
,
showingLogs
:
right
.
showingLogs
,
showingTable
:
right
.
showingTable
,
dedupStrategy
:
right
.
dedupStrategy
,
},
};
urlStates
.
right
=
serializeStateToUrlParam
(
rightUrlState
,
true
);
...
...
@@ -696,24 +714,26 @@ const togglePanelActionCreator = (
|
ActionCreator
<
ToggleGraphPayload
>
|
ActionCreator
<
ToggleLogsPayload
>
|
ActionCreator
<
ToggleTablePayload
>
)
=>
(
exploreId
:
ExploreId
)
=>
{
return
(
dispatch
,
getState
)
=>
{
let
shouldRunQueries
;
dispatch
(
actionCreator
({
exploreId
}));
dispatch
(
stateSave
());
)
=>
(
exploreId
:
ExploreId
,
isPanelVisible
:
boolean
)
=>
{
return
dispatch
=>
{
let
uiFragmentStateUpdate
:
Partial
<
ExploreUIState
>
;
const
shouldRunQueries
=
!
isPanelVisible
;
switch
(
actionCreator
.
type
)
{
case
toggleGraphAction
.
type
:
shouldRunQueries
=
getState
().
explore
[
exploreId
].
showingGraph
;
uiFragmentStateUpdate
=
{
showingGraph
:
!
isPanelVisible
}
;
break
;
case
toggleLogsAction
.
type
:
shouldRunQueries
=
getState
().
explore
[
exploreId
].
showingLogs
;
uiFragmentStateUpdate
=
{
showingLogs
:
!
isPanelVisible
}
;
break
;
case
toggleTableAction
.
type
:
shouldRunQueries
=
getState
().
explore
[
exploreId
].
showingTable
;
uiFragmentStateUpdate
=
{
showingTable
:
!
isPanelVisible
}
;
break
;
}
dispatch
(
actionCreator
({
exploreId
}));
dispatch
(
updateExploreUIState
(
exploreId
,
uiFragmentStateUpdate
));
if
(
shouldRunQueries
)
{
dispatch
(
runQueries
(
exploreId
));
}
...
...
@@ -734,3 +754,12 @@ export const toggleLogs = togglePanelActionCreator(toggleLogsAction);
* Expand/collapse the table result viewer. When collapsed, table queries won't be run.
*/
export
const
toggleTable
=
togglePanelActionCreator
(
toggleTableAction
);
/**
* Change logs deduplication strategy and update URL.
*/
export
const
changeDedupStrategy
=
(
exploreId
,
dedupStrategy
:
LogsDedupStrategy
)
=>
{
return
dispatch
=>
{
dispatch
(
updateExploreUIState
(
exploreId
,
{
dedupStrategy
}));
};
};
public/app/features/explore/state/reducers.ts
View file @
a79bd424
...
...
@@ -37,6 +37,7 @@ import {
toggleLogsAction
,
toggleTableAction
,
queriesImportedAction
,
updateUIStateAction
,
}
from
'./actionTypes'
;
export
const
DEFAULT_RANGE
=
{
...
...
@@ -407,6 +408,12 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
},
})
.
addMapper
({
filter
:
updateUIStateAction
,
mapper
:
(
state
,
action
):
ExploreItemState
=>
{
return
{
...
state
,
...
action
.
payload
};
},
})
.
addMapper
({
filter
:
toggleGraphAction
,
mapper
:
(
state
):
ExploreItemState
=>
{
const
showingGraph
=
!
state
.
showingGraph
;
...
...
@@ -415,7 +422,7 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
// Discard transactions related to Graph query
nextQueryTransactions
=
state
.
queryTransactions
.
filter
(
qt
=>
qt
.
resultType
!==
'Graph'
);
}
return
{
...
state
,
queryTransactions
:
nextQueryTransactions
,
showingGraph
};
return
{
...
state
,
queryTransactions
:
nextQueryTransactions
};
},
})
.
addMapper
({
...
...
@@ -427,7 +434,7 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
// Discard transactions related to Logs query
nextQueryTransactions
=
state
.
queryTransactions
.
filter
(
qt
=>
qt
.
resultType
!==
'Logs'
);
}
return
{
...
state
,
queryTransactions
:
nextQueryTransactions
,
showingLogs
};
return
{
...
state
,
queryTransactions
:
nextQueryTransactions
};
},
})
.
addMapper
({
...
...
@@ -435,7 +442,7 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
mapper
:
(
state
):
ExploreItemState
=>
{
const
showingTable
=
!
state
.
showingTable
;
if
(
showingTable
)
{
return
{
...
state
,
showingTable
,
queryTransactions
:
state
.
queryTransactions
};
return
{
...
state
,
queryTransactions
:
state
.
queryTransactions
};
}
// Toggle off needs discarding of table queries and results
...
...
@@ -446,7 +453,7 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
state
.
queryIntervals
.
intervalMs
);
return
{
...
state
,
...
results
,
queryTransactions
:
nextQueryTransactions
,
showingTable
};
return
{
...
state
,
...
results
,
queryTransactions
:
nextQueryTransactions
};
},
})
.
addMapper
({
...
...
public/app/features/panel/specs/metrics_panel_ctrl.test.ts
View file @
a79bd424
jest
.
mock
(
'app/core/core'
,
()
=>
({}));
jest
.
mock
(
'app/core/config'
,
()
=>
{
return
{
bootData
:
{
user
:
{},
},
panels
:
{
test
:
{
id
:
'test'
,
...
...
public/app/types/explore.ts
View file @
a79bd424
...
...
@@ -11,7 +11,7 @@ import {
}
from
'@grafana/ui'
;
import
{
Emitter
}
from
'app/core/core'
;
import
{
LogsModel
}
from
'app/core/logs_model'
;
import
{
LogsModel
,
LogsDedupStrategy
}
from
'app/core/logs_model'
;
import
TableModel
from
'app/core/table_model'
;
export
interface
CompletionItem
{
...
...
@@ -237,12 +237,18 @@ export interface ExploreItemState {
* React keys for rendering of QueryRows
*/
queryKeys
:
string
[];
/**
* Current logs deduplication strategy
*/
dedupStrategy
?:
LogsDedupStrategy
;
}
export
interface
ExploreUIState
{
showingTable
:
boolean
;
showingGraph
:
boolean
;
showingLogs
:
boolean
;
dedupStrategy
?:
LogsDedupStrategy
;
}
export
interface
ExploreUrlState
{
...
...
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