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
34761205
Unverified
Commit
34761205
authored
Aug 06, 2018
by
David
Committed by
GitHub
Aug 06, 2018
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #12799 from grafana/davkal/explore-history
Explore: Add history to query fields
parents
a73fc4a6
cda3b017
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
135 additions
and
6 deletions
+135
-6
public/app/containers/Explore/Explore.tsx
+39
-1
public/app/containers/Explore/PromQueryField.tsx
+42
-2
public/app/containers/Explore/QueryField.tsx
+6
-0
public/app/containers/Explore/QueryRows.tsx
+4
-3
public/app/core/specs/store.jest.ts
+12
-0
public/app/core/store.ts
+32
-0
No files found.
public/app/containers/Explore/Explore.tsx
View file @
34761205
...
@@ -4,6 +4,7 @@ import Select from 'react-select';
...
@@ -4,6 +4,7 @@ import Select from 'react-select';
import
kbn
from
'app/core/utils/kbn'
;
import
kbn
from
'app/core/utils/kbn'
;
import
colors
from
'app/core/utils/colors'
;
import
colors
from
'app/core/utils/colors'
;
import
store
from
'app/core/store'
;
import
TimeSeries
from
'app/core/time_series2'
;
import
TimeSeries
from
'app/core/time_series2'
;
import
{
decodePathComponent
}
from
'app/core/utils/location_util'
;
import
{
decodePathComponent
}
from
'app/core/utils/location_util'
;
import
{
parse
as
parseDate
}
from
'app/core/utils/datemath'
;
import
{
parse
as
parseDate
}
from
'app/core/utils/datemath'
;
...
@@ -16,6 +17,8 @@ import Table from './Table';
...
@@ -16,6 +17,8 @@ import Table from './Table';
import
TimePicker
,
{
DEFAULT_RANGE
}
from
'./TimePicker'
;
import
TimePicker
,
{
DEFAULT_RANGE
}
from
'./TimePicker'
;
import
{
ensureQueries
,
generateQueryKey
,
hasQuery
}
from
'./utils/query'
;
import
{
ensureQueries
,
generateQueryKey
,
hasQuery
}
from
'./utils/query'
;
const
MAX_HISTORY_ITEMS
=
100
;
function
makeTimeSeriesList
(
dataList
,
options
)
{
function
makeTimeSeriesList
(
dataList
,
options
)
{
return
dataList
.
map
((
seriesData
,
index
)
=>
{
return
dataList
.
map
((
seriesData
,
index
)
=>
{
const
datapoints
=
seriesData
.
datapoints
||
[];
const
datapoints
=
seriesData
.
datapoints
||
[];
...
@@ -56,6 +59,7 @@ interface IExploreState {
...
@@ -56,6 +59,7 @@ interface IExploreState {
datasourceLoading
:
boolean
|
null
;
datasourceLoading
:
boolean
|
null
;
datasourceMissing
:
boolean
;
datasourceMissing
:
boolean
;
graphResult
:
any
;
graphResult
:
any
;
history
:
any
[];
initialDatasource
?:
string
;
initialDatasource
?:
string
;
latency
:
number
;
latency
:
number
;
loading
:
any
;
loading
:
any
;
...
@@ -86,6 +90,7 @@ export class Explore extends React.Component<any, IExploreState> {
...
@@ -86,6 +90,7 @@ export class Explore extends React.Component<any, IExploreState> {
datasourceMissing
:
false
,
datasourceMissing
:
false
,
graphResult
:
null
,
graphResult
:
null
,
initialDatasource
:
datasource
,
initialDatasource
:
datasource
,
history
:
[],
latency
:
0
,
latency
:
0
,
loading
:
false
,
loading
:
false
,
logsResult
:
null
,
logsResult
:
null
,
...
@@ -138,6 +143,7 @@ export class Explore extends React.Component<any, IExploreState> {
...
@@ -138,6 +143,7 @@ export class Explore extends React.Component<any, IExploreState> {
const
supportsGraph
=
datasource
.
meta
.
metrics
;
const
supportsGraph
=
datasource
.
meta
.
metrics
;
const
supportsLogs
=
datasource
.
meta
.
logs
;
const
supportsLogs
=
datasource
.
meta
.
logs
;
const
supportsTable
=
datasource
.
meta
.
metrics
;
const
supportsTable
=
datasource
.
meta
.
metrics
;
const
datasourceId
=
datasource
.
meta
.
id
;
let
datasourceError
=
null
;
let
datasourceError
=
null
;
try
{
try
{
...
@@ -147,10 +153,14 @@ export class Explore extends React.Component<any, IExploreState> {
...
@@ -147,10 +153,14 @@ export class Explore extends React.Component<any, IExploreState> {
datasourceError
=
(
error
&&
error
.
statusText
)
||
error
;
datasourceError
=
(
error
&&
error
.
statusText
)
||
error
;
}
}
const
historyKey
=
`grafana.explore.history.
${
datasourceId
}
`
;
const
history
=
store
.
getObject
(
historyKey
,
[]);
this
.
setState
(
this
.
setState
(
{
{
datasource
,
datasource
,
datasourceError
,
datasourceError
,
history
,
supportsGraph
,
supportsGraph
,
supportsLogs
,
supportsLogs
,
supportsTable
,
supportsTable
,
...
@@ -269,6 +279,27 @@ export class Explore extends React.Component<any, IExploreState> {
...
@@ -269,6 +279,27 @@ export class Explore extends React.Component<any, IExploreState> {
}
}
};
};
onQuerySuccess
(
datasourceId
:
string
,
queries
:
any
[]):
void
{
// save queries to history
let
{
datasource
,
history
}
=
this
.
state
;
if
(
datasource
.
meta
.
id
!==
datasourceId
)
{
// Navigated away, queries did not matter
return
;
}
const
ts
=
Date
.
now
();
queries
.
forEach
(
q
=>
{
const
{
query
}
=
q
;
history
=
[{
query
,
ts
},
...
history
];
});
if
(
history
.
length
>
MAX_HISTORY_ITEMS
)
{
history
=
history
.
slice
(
0
,
MAX_HISTORY_ITEMS
);
}
// Combine all queries of a datasource type into one history
const
historyKey
=
`grafana.explore.history.
${
datasourceId
}
`
;
store
.
setObject
(
historyKey
,
history
);
this
.
setState
({
history
});
}
buildQueryOptions
(
targetOptions
:
{
format
:
string
;
instant
?:
boolean
})
{
buildQueryOptions
(
targetOptions
:
{
format
:
string
;
instant
?:
boolean
})
{
const
{
datasource
,
queries
,
range
}
=
this
.
state
;
const
{
datasource
,
queries
,
range
}
=
this
.
state
;
const
resolution
=
this
.
el
.
offsetWidth
;
const
resolution
=
this
.
el
.
offsetWidth
;
...
@@ -301,6 +332,7 @@ export class Explore extends React.Component<any, IExploreState> {
...
@@ -301,6 +332,7 @@ export class Explore extends React.Component<any, IExploreState> {
const
result
=
makeTimeSeriesList
(
res
.
data
,
options
);
const
result
=
makeTimeSeriesList
(
res
.
data
,
options
);
const
latency
=
Date
.
now
()
-
now
;
const
latency
=
Date
.
now
()
-
now
;
this
.
setState
({
latency
,
loading
:
false
,
graphResult
:
result
,
requestOptions
:
options
});
this
.
setState
({
latency
,
loading
:
false
,
graphResult
:
result
,
requestOptions
:
options
});
this
.
onQuerySuccess
(
datasource
.
meta
.
id
,
queries
);
}
catch
(
response
)
{
}
catch
(
response
)
{
console
.
error
(
response
);
console
.
error
(
response
);
const
queryError
=
response
.
data
?
response
.
data
.
error
:
response
;
const
queryError
=
response
.
data
?
response
.
data
.
error
:
response
;
...
@@ -324,6 +356,7 @@ export class Explore extends React.Component<any, IExploreState> {
...
@@ -324,6 +356,7 @@ export class Explore extends React.Component<any, IExploreState> {
const
tableModel
=
res
.
data
[
0
];
const
tableModel
=
res
.
data
[
0
];
const
latency
=
Date
.
now
()
-
now
;
const
latency
=
Date
.
now
()
-
now
;
this
.
setState
({
latency
,
loading
:
false
,
tableResult
:
tableModel
,
requestOptions
:
options
});
this
.
setState
({
latency
,
loading
:
false
,
tableResult
:
tableModel
,
requestOptions
:
options
});
this
.
onQuerySuccess
(
datasource
.
meta
.
id
,
queries
);
}
catch
(
response
)
{
}
catch
(
response
)
{
console
.
error
(
response
);
console
.
error
(
response
);
const
queryError
=
response
.
data
?
response
.
data
.
error
:
response
;
const
queryError
=
response
.
data
?
response
.
data
.
error
:
response
;
...
@@ -347,6 +380,7 @@ export class Explore extends React.Component<any, IExploreState> {
...
@@ -347,6 +380,7 @@ export class Explore extends React.Component<any, IExploreState> {
const
logsData
=
res
.
data
;
const
logsData
=
res
.
data
;
const
latency
=
Date
.
now
()
-
now
;
const
latency
=
Date
.
now
()
-
now
;
this
.
setState
({
latency
,
loading
:
false
,
logsResult
:
logsData
,
requestOptions
:
options
});
this
.
setState
({
latency
,
loading
:
false
,
logsResult
:
logsData
,
requestOptions
:
options
});
this
.
onQuerySuccess
(
datasource
.
meta
.
id
,
queries
);
}
catch
(
response
)
{
}
catch
(
response
)
{
console
.
error
(
response
);
console
.
error
(
response
);
const
queryError
=
response
.
data
?
response
.
data
.
error
:
response
;
const
queryError
=
response
.
data
?
response
.
data
.
error
:
response
;
...
@@ -367,6 +401,7 @@ export class Explore extends React.Component<any, IExploreState> {
...
@@ -367,6 +401,7 @@ export class Explore extends React.Component<any, IExploreState> {
datasourceLoading
,
datasourceLoading
,
datasourceMissing
,
datasourceMissing
,
graphResult
,
graphResult
,
history
,
latency
,
latency
,
loading
,
loading
,
logsResult
,
logsResult
,
...
@@ -470,6 +505,7 @@ export class Explore extends React.Component<any, IExploreState> {
...
@@ -470,6 +505,7 @@ export class Explore extends React.Component<any, IExploreState> {
{
datasource
&&
!
datasourceError
?
(
{
datasource
&&
!
datasourceError
?
(
<
div
className=
"explore-container"
>
<
div
className=
"explore-container"
>
<
QueryRows
<
QueryRows
history=
{
history
}
queries=
{
queries
}
queries=
{
queries
}
request=
{
this
.
request
}
request=
{
this
.
request
}
onAddQueryRow=
{
this
.
handleAddQueryRow
}
onAddQueryRow=
{
this
.
handleAddQueryRow
}
...
@@ -488,7 +524,9 @@ export class Explore extends React.Component<any, IExploreState> {
...
@@ -488,7 +524,9 @@ export class Explore extends React.Component<any, IExploreState> {
split=
{
split
}
split=
{
split
}
/>
/>
)
:
null
}
)
:
null
}
{
supportsTable
&&
showingTable
?
<
Table
data=
{
tableResult
}
onClickCell=
{
this
.
onClickTableCell
}
className=
"m-t-3"
/>
:
null
}
{
supportsTable
&&
showingTable
?
(
<
Table
data=
{
tableResult
}
onClickCell=
{
this
.
onClickTableCell
}
className=
"m-t-3"
/>
)
:
null
}
{
supportsLogs
&&
showingLogs
?
<
Logs
data=
{
logsResult
}
/>
:
null
}
{
supportsLogs
&&
showingLogs
?
<
Logs
data=
{
logsResult
}
/>
:
null
}
</
main
>
</
main
>
</
div
>
</
div
>
...
...
public/app/containers/Explore/PromQueryField.tsx
View file @
34761205
import
_
from
'lodash'
;
import
_
from
'lodash'
;
import
moment
from
'moment'
;
import
React
from
'react'
;
import
React
from
'react'
;
import
{
Value
}
from
'slate'
;
import
{
Value
}
from
'slate'
;
...
@@ -19,6 +20,8 @@ import TypeaheadField, {
...
@@ -19,6 +20,8 @@ import TypeaheadField, {
const
DEFAULT_KEYS
=
[
'job'
,
'instance'
];
const
DEFAULT_KEYS
=
[
'job'
,
'instance'
];
const
EMPTY_SELECTOR
=
'{}'
;
const
EMPTY_SELECTOR
=
'{}'
;
const
HISTORY_ITEM_COUNT
=
5
;
const
HISTORY_COUNT_CUTOFF
=
1000
*
60
*
60
*
24
;
// 24h
const
METRIC_MARK
=
'metric'
;
const
METRIC_MARK
=
'metric'
;
const
PRISM_LANGUAGE
=
'promql'
;
const
PRISM_LANGUAGE
=
'promql'
;
...
@@ -28,6 +31,22 @@ export const setFunctionMove = (suggestion: Suggestion): Suggestion => {
...
@@ -28,6 +31,22 @@ export const setFunctionMove = (suggestion: Suggestion): Suggestion => {
return
suggestion
;
return
suggestion
;
};
};
export
function
addHistoryMetadata
(
item
:
Suggestion
,
history
:
any
[]):
Suggestion
{
const
cutoffTs
=
Date
.
now
()
-
HISTORY_COUNT_CUTOFF
;
const
historyForItem
=
history
.
filter
(
h
=>
h
.
ts
>
cutoffTs
&&
h
.
query
===
item
.
label
);
const
count
=
historyForItem
.
length
;
const
recent
=
historyForItem
[
0
];
let
hint
=
`Queried
${
count
}
times in the last 24h.`
;
if
(
recent
)
{
const
lastQueried
=
moment
(
recent
.
ts
).
fromNow
();
hint
=
`
${
hint
}
Last queried
${
lastQueried
}
.`
;
}
return
{
...
item
,
documentation
:
hint
,
};
}
export
function
willApplySuggestion
(
export
function
willApplySuggestion
(
suggestion
:
string
,
suggestion
:
string
,
{
typeaheadContext
,
typeaheadText
}:
TypeaheadFieldState
{
typeaheadContext
,
typeaheadText
}:
TypeaheadFieldState
...
@@ -59,6 +78,7 @@ export function willApplySuggestion(
...
@@ -59,6 +78,7 @@ export function willApplySuggestion(
}
}
interface
PromQueryFieldProps
{
interface
PromQueryFieldProps
{
history
?:
any
[];
initialQuery
?:
string
|
null
;
initialQuery
?:
string
|
null
;
labelKeys
?:
{
[
index
:
string
]:
string
[]
};
// metric -> [labelKey,...]
labelKeys
?:
{
[
index
:
string
]:
string
[]
};
// metric -> [labelKey,...]
labelValues
?:
{
[
index
:
string
]:
{
[
index
:
string
]:
string
[]
}
};
// metric -> labelKey -> [labelValue,...]
labelValues
?:
{
[
index
:
string
]:
{
[
index
:
string
]:
string
[]
}
};
// metric -> labelKey -> [labelValue,...]
...
@@ -162,17 +182,37 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
...
@@ -162,17 +182,37 @@ class PromQueryField extends React.Component<PromQueryFieldProps, PromQueryField
}
}
getEmptyTypeahead
():
TypeaheadOutput
{
getEmptyTypeahead
():
TypeaheadOutput
{
const
{
history
}
=
this
.
props
;
const
{
metrics
}
=
this
.
state
;
const
suggestions
:
SuggestionGroup
[]
=
[];
const
suggestions
:
SuggestionGroup
[]
=
[];
if
(
history
&&
history
.
length
>
0
)
{
const
historyItems
=
_
.
chain
(
history
)
.
uniqBy
(
'query'
)
.
take
(
HISTORY_ITEM_COUNT
)
.
map
(
h
=>
h
.
query
)
.
map
(
wrapLabel
)
.
map
(
item
=>
addHistoryMetadata
(
item
,
history
))
.
value
();
suggestions
.
push
({
prefixMatch
:
true
,
skipSort
:
true
,
label
:
'History'
,
items
:
historyItems
,
});
}
suggestions
.
push
({
suggestions
.
push
({
prefixMatch
:
true
,
prefixMatch
:
true
,
label
:
'Functions'
,
label
:
'Functions'
,
items
:
FUNCTIONS
.
map
(
setFunctionMove
),
items
:
FUNCTIONS
.
map
(
setFunctionMove
),
});
});
if
(
this
.
state
.
metrics
)
{
if
(
metrics
)
{
suggestions
.
push
({
suggestions
.
push
({
label
:
'Metrics'
,
label
:
'Metrics'
,
items
:
this
.
state
.
metrics
.
map
(
wrapLabel
),
items
:
metrics
.
map
(
wrapLabel
),
});
});
}
}
return
{
suggestions
};
return
{
suggestions
};
...
...
public/app/containers/Explore/QueryField.tsx
View file @
34761205
...
@@ -97,6 +97,10 @@ export interface SuggestionGroup {
...
@@ -97,6 +97,10 @@ export interface SuggestionGroup {
* If true, do not filter items in this group based on the search.
* If true, do not filter items in this group based on the search.
*/
*/
skipFilter
?:
boolean
;
skipFilter
?:
boolean
;
/**
* If true, do not sort items.
*/
skipSort
?:
boolean
;
}
}
interface
TypeaheadFieldProps
{
interface
TypeaheadFieldProps
{
...
@@ -244,8 +248,10 @@ class QueryField extends React.Component<TypeaheadFieldProps, TypeaheadFieldStat
...
@@ -244,8 +248,10 @@ class QueryField extends React.Component<TypeaheadFieldProps, TypeaheadFieldStat
group
.
items
=
group
.
items
.
filter
(
c
=>
c
.
insertText
||
(
c
.
filterText
||
c
.
label
)
!==
prefix
);
group
.
items
=
group
.
items
.
filter
(
c
=>
c
.
insertText
||
(
c
.
filterText
||
c
.
label
)
!==
prefix
);
}
}
if
(
!
group
.
skipSort
)
{
group
.
items
=
_
.
sortBy
(
group
.
items
,
item
=>
item
.
sortText
||
item
.
label
);
group
.
items
=
_
.
sortBy
(
group
.
items
,
item
=>
item
.
sortText
||
item
.
label
);
}
}
}
return
group
;
return
group
;
})
})
.
filter
(
group
=>
group
.
items
&&
group
.
items
.
length
>
0
);
// Filter out empty groups
.
filter
(
group
=>
group
.
items
&&
group
.
items
.
length
>
0
);
// Filter out empty groups
...
...
public/app/containers/Explore/QueryRows.tsx
View file @
34761205
...
@@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
...
@@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
import
QueryField
from
'./PromQueryField'
;
import
QueryField
from
'./PromQueryField'
;
class
QueryRow
extends
PureComponent
<
any
,
any
>
{
class
QueryRow
extends
PureComponent
<
any
,
{}
>
{
handleChangeQuery
=
value
=>
{
handleChangeQuery
=
value
=>
{
const
{
index
,
onChangeQuery
}
=
this
.
props
;
const
{
index
,
onChangeQuery
}
=
this
.
props
;
if
(
onChangeQuery
)
{
if
(
onChangeQuery
)
{
...
@@ -32,7 +32,7 @@ class QueryRow extends PureComponent<any, any> {
...
@@ -32,7 +32,7 @@ class QueryRow extends PureComponent<any, any> {
};
};
render
()
{
render
()
{
const
{
request
,
query
,
edited
}
=
this
.
props
;
const
{
edited
,
history
,
query
,
request
}
=
this
.
props
;
return
(
return
(
<
div
className=
"query-row"
>
<
div
className=
"query-row"
>
<
div
className=
"query-row-tools"
>
<
div
className=
"query-row-tools"
>
...
@@ -46,6 +46,7 @@ class QueryRow extends PureComponent<any, any> {
...
@@ -46,6 +46,7 @@ class QueryRow extends PureComponent<any, any> {
<
div
className=
"slate-query-field-wrapper"
>
<
div
className=
"slate-query-field-wrapper"
>
<
QueryField
<
QueryField
initialQuery=
{
edited
?
null
:
query
}
initialQuery=
{
edited
?
null
:
query
}
history=
{
history
}
portalPrefix=
"explore"
portalPrefix=
"explore"
onPressEnter=
{
this
.
handlePressEnter
}
onPressEnter=
{
this
.
handlePressEnter
}
onQueryChange=
{
this
.
handleChangeQuery
}
onQueryChange=
{
this
.
handleChangeQuery
}
...
@@ -57,7 +58,7 @@ class QueryRow extends PureComponent<any, any> {
...
@@ -57,7 +58,7 @@ class QueryRow extends PureComponent<any, any> {
}
}
}
}
export
default
class
QueryRows
extends
PureComponent
<
any
,
any
>
{
export
default
class
QueryRows
extends
PureComponent
<
any
,
{}
>
{
render
()
{
render
()
{
const
{
className
=
''
,
queries
,
...
handlers
}
=
this
.
props
;
const
{
className
=
''
,
queries
,
...
handlers
}
=
this
.
props
;
return
(
return
(
...
...
public/app/core/specs/store.jest.ts
View file @
34761205
...
@@ -32,6 +32,18 @@ describe('store', () => {
...
@@ -32,6 +32,18 @@ describe('store', () => {
expect
(
store
.
getBool
(
'key5'
,
false
)).
toBe
(
true
);
expect
(
store
.
getBool
(
'key5'
,
false
)).
toBe
(
true
);
});
});
it
(
'gets an object'
,
()
=>
{
expect
(
store
.
getObject
(
'object1'
)).
toBeUndefined
();
expect
(
store
.
getObject
(
'object1'
,
[])).
toEqual
([]);
store
.
setObject
(
'object1'
,
[
1
]);
expect
(
store
.
getObject
(
'object1'
)).
toEqual
([
1
]);
});
it
(
'sets an object'
,
()
=>
{
expect
(
store
.
setObject
(
'object2'
,
{
a
:
1
})).
toBe
(
true
);
expect
(
store
.
getObject
(
'object2'
)).
toEqual
({
a
:
1
});
});
it
(
'key should be deleted'
,
()
=>
{
it
(
'key should be deleted'
,
()
=>
{
store
.
set
(
'key6'
,
'123'
);
store
.
set
(
'key6'
,
'123'
);
store
.
delete
(
'key6'
);
store
.
delete
(
'key6'
);
...
...
public/app/core/store.ts
View file @
34761205
...
@@ -14,6 +14,38 @@ export class Store {
...
@@ -14,6 +14,38 @@ export class Store {
return
window
.
localStorage
[
key
]
===
'true'
;
return
window
.
localStorage
[
key
]
===
'true'
;
}
}
getObject
(
key
:
string
,
def
?:
any
)
{
let
ret
=
def
;
if
(
this
.
exists
(
key
))
{
const
json
=
window
.
localStorage
[
key
];
try
{
ret
=
JSON
.
parse
(
json
);
}
catch
(
error
)
{
console
.
error
(
`Error parsing store object:
${
key
}
. Returning default:
${
def
}
. [
${
error
}
]`
);
}
}
return
ret
;
}
// Returns true when successfully stored
setObject
(
key
:
string
,
value
:
any
):
boolean
{
let
json
;
try
{
json
=
JSON
.
stringify
(
value
);
}
catch
(
error
)
{
console
.
error
(
`Could not stringify object:
${
key
}
. [
${
error
}
]`
);
return
false
;
}
try
{
this
.
set
(
key
,
json
);
}
catch
(
error
)
{
// Likely hitting storage quota
console
.
error
(
`Could not save item in localStorage:
${
key
}
. [
${
error
}
]`
);
return
false
;
}
return
true
;
}
exists
(
key
)
{
exists
(
key
)
{
return
window
.
localStorage
[
key
]
!==
void
0
;
return
window
.
localStorage
[
key
]
!==
void
0
;
}
}
...
...
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