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
c5da1864
Unverified
Commit
c5da1864
authored
Jan 15, 2020
by
Ryan McKinley
Committed by
GitHub
Jan 15, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
AppPlugin: remove simple app from the core repo (#21526)
parent
f6130db0
Show whitespace changes
Inline
Side-by-side
Showing
17 changed files
with
1 additions
and
618 deletions
+1
-618
docs/sources/plugins/developing/apps.md
+1
-1
public/app/features/plugins/built_in_plugins.ts
+0
-4
public/app/features/plugins/plugin_loader.test.ts
+0
-110
public/app/plugins/app/example-app/ExampleRootPage.tsx
+0
-103
public/app/plugins/app/example-app/README.md
+0
-4
public/app/plugins/app/example-app/config/ExamplePage1.tsx
+0
-26
public/app/plugins/app/example-app/config/ExamplePage2.tsx
+0
-26
public/app/plugins/app/example-app/dashboards/stats.json
+0
-110
public/app/plugins/app/example-app/dashboards/streaming.json
+0
-83
public/app/plugins/app/example-app/img/logo.png
+0
-0
public/app/plugins/app/example-app/legacy/angular_example_page.html
+0
-8
public/app/plugins/app/example-app/legacy/angular_example_page.ts
+0
-10
public/app/plugins/app/example-app/legacy/config.html
+0
-22
public/app/plugins/app/example-app/legacy/config.ts
+0
-36
public/app/plugins/app/example-app/module.ts
+0
-29
public/app/plugins/app/example-app/plugin.json
+0
-42
public/app/plugins/app/example-app/types.ts
+0
-4
No files found.
docs/sources/plugins/developing/apps.md
View file @
c5da1864
...
@@ -55,5 +55,5 @@ If possible a link to a dashboard or custom page should be shown after enabling
...
@@ -55,5 +55,5 @@ If possible a link to a dashboard or custom page should be shown after enabling
> Our goal is not to have a very extensive documentation but rather have actual
> Our goal is not to have a very extensive documentation but rather have actual
> code that people can look at. An example implementation of an app can be found
> code that people can look at. An example implementation of an app can be found
> in this [example app repo](https://github.com/grafana/
grafana/tree/master/public/app/plugins/app/example-app
)
> in this [example app repo](https://github.com/grafana/
simple-app-plugin
)
public/app/features/plugins/built_in_plugins.ts
View file @
c5da1864
...
@@ -54,8 +54,6 @@ import * as barGaugePanel from 'app/plugins/panel/bargauge/module';
...
@@ -54,8 +54,6 @@ import * as barGaugePanel from 'app/plugins/panel/bargauge/module';
import
*
as
logsPanel
from
'app/plugins/panel/logs/module'
;
import
*
as
logsPanel
from
'app/plugins/panel/logs/module'
;
import
*
as
newsPanel
from
'app/plugins/panel/news/module'
;
import
*
as
newsPanel
from
'app/plugins/panel/news/module'
;
const
exampleApp
=
async
()
=>
await
import
(
/* webpackChunkName: "exampleApp" */
'app/plugins/app/example-app/module'
);
const
builtInPlugins
:
any
=
{
const
builtInPlugins
:
any
=
{
'app/plugins/datasource/graphite/module'
:
graphitePlugin
,
'app/plugins/datasource/graphite/module'
:
graphitePlugin
,
'app/plugins/datasource/cloudwatch/module'
:
cloudwatchPlugin
,
'app/plugins/datasource/cloudwatch/module'
:
cloudwatchPlugin
,
...
@@ -94,8 +92,6 @@ const builtInPlugins: any = {
...
@@ -94,8 +92,6 @@ const builtInPlugins: any = {
'app/plugins/panel/piechart/module'
:
pieChartPanel
,
'app/plugins/panel/piechart/module'
:
pieChartPanel
,
'app/plugins/panel/bargauge/module'
:
barGaugePanel
,
'app/plugins/panel/bargauge/module'
:
barGaugePanel
,
'app/plugins/panel/logs/module'
:
logsPanel
,
'app/plugins/panel/logs/module'
:
logsPanel
,
'app/plugins/app/example-app/module'
:
exampleApp
,
};
};
export
default
builtInPlugins
;
export
default
builtInPlugins
;
public/app/features/plugins/plugin_loader.test.ts
deleted
100644 → 0
View file @
f6130db0
// Use the real plugin_loader (stubbed by default)
jest
.
unmock
(
'app/features/plugins/plugin_loader'
);
(
global
as
any
).
ace
=
{
define
:
jest
.
fn
(),
};
jest
.
mock
(
'app/core/core'
,
()
=>
{
return
{
coreModule
:
{
directive
:
jest
.
fn
(),
},
};
});
import
{
SystemJS
}
from
'@grafana/runtime'
;
import
{
AppPluginMeta
,
PluginMetaInfo
,
PluginType
,
PluginIncludeType
,
AppPlugin
}
from
'@grafana/data'
;
import
{
importAppPlugin
}
from
'./plugin_loader'
;
class
MyCustomApp
extends
AppPlugin
{
initWasCalled
=
false
;
calledTwice
=
false
;
init
(
meta
:
AppPluginMeta
)
{
this
.
initWasCalled
=
true
;
this
.
calledTwice
=
this
.
meta
===
meta
;
}
}
describe
(
'Load App'
,
()
=>
{
const
app
=
new
MyCustomApp
();
const
modulePath
=
'my/custom/plugin/module'
;
beforeAll
(()
=>
{
SystemJS
.
set
(
modulePath
,
SystemJS
.
newModule
({
plugin
:
app
}));
});
afterAll
(()
=>
{
SystemJS
.
delete
(
modulePath
);
});
it
(
'should call init and set meta'
,
async
()
=>
{
const
meta
:
AppPluginMeta
=
{
id
:
'test-app'
,
module
:
modulePath
,
baseUrl
:
'xxx'
,
info
:
{}
as
PluginMetaInfo
,
type
:
PluginType
.
app
,
name
:
'test'
,
};
// Check that we mocked the import OK
const
m
=
await
SystemJS
.
import
(
modulePath
);
expect
(
m
.
plugin
).
toBe
(
app
);
const
loaded
=
await
importAppPlugin
(
meta
);
expect
(
loaded
).
toBe
(
app
);
expect
(
app
.
meta
).
toBe
(
meta
);
expect
(
app
.
initWasCalled
).
toBeTruthy
();
expect
(
app
.
calledTwice
).
toBeFalsy
();
const
again
=
await
importAppPlugin
(
meta
);
expect
(
again
).
toBe
(
app
);
expect
(
app
.
calledTwice
).
toBeTruthy
();
});
});
import
{
ExampleConfigCtrl
as
ConfigCtrl
}
from
'app/plugins/app/example-app/legacy/config'
;
import
{
AngularExamplePageCtrl
}
from
'app/plugins/app/example-app/legacy/angular_example_page'
;
describe
(
'Load Legacy App'
,
()
=>
{
const
app
=
{
ConfigCtrl
,
AngularExamplePageCtrl
,
// Must match `pages.component` in plugin.json
};
const
modulePath
=
'my/custom/legacy/plugin/module'
;
beforeAll
(()
=>
{
SystemJS
.
set
(
modulePath
,
SystemJS
.
newModule
(
app
));
});
afterAll
(()
=>
{
SystemJS
.
delete
(
modulePath
);
});
it
(
'should call init and set meta for legacy app'
,
async
()
=>
{
const
meta
:
AppPluginMeta
=
{
id
:
'test-app'
,
module
:
modulePath
,
baseUrl
:
'xxx'
,
info
:
{}
as
PluginMetaInfo
,
type
:
PluginType
.
app
,
name
:
'test'
,
includes
:
[
{
type
:
PluginIncludeType
.
page
,
name
:
'Example Page'
,
component
:
'AngularExamplePageCtrl'
,
role
:
'Viewer'
,
addToNav
:
false
,
},
],
};
const
loaded
=
await
importAppPlugin
(
meta
);
expect
(
loaded
).
toHaveProperty
(
'angularPages'
);
expect
(
loaded
.
angularPages
).
toHaveProperty
(
'AngularExamplePageCtrl'
,
AngularExamplePageCtrl
);
});
});
public/app/plugins/app/example-app/ExampleRootPage.tsx
deleted
100644 → 0
View file @
f6130db0
// Libraries
import
React
,
{
PureComponent
}
from
'react'
;
// Types
import
{
NavModelItem
,
AppRootProps
}
from
'@grafana/data'
;
interface
Props
extends
AppRootProps
{}
const
TAB_ID_A
=
'A'
;
const
TAB_ID_B
=
'B'
;
const
TAB_ID_C
=
'C'
;
export
class
ExampleRootPage
<
ExampleAppSettings
>
extends
PureComponent
<
Props
>
{
constructor
(
props
:
Props
)
{
super
(
props
);
}
componentDidMount
()
{
this
.
updateNav
();
}
componentDidUpdate
(
prevProps
:
Props
)
{
if
(
this
.
props
.
query
!==
prevProps
.
query
)
{
if
(
this
.
props
.
query
.
tab
!==
prevProps
.
query
.
tab
)
{
this
.
updateNav
();
}
}
}
updateNav
()
{
const
{
path
,
onNavChanged
,
query
,
meta
}
=
this
.
props
;
const
tabs
:
NavModelItem
[]
=
[];
tabs
.
push
({
text
:
'Tab A'
,
icon
:
'fa fa-fw fa-file-text-o'
,
url
:
path
+
'?tab='
+
TAB_ID_A
,
id
:
TAB_ID_A
,
});
tabs
.
push
({
text
:
'Tab B'
,
icon
:
'fa fa-fw fa-file-text-o'
,
url
:
path
+
'?tab='
+
TAB_ID_B
,
id
:
TAB_ID_B
,
});
tabs
.
push
({
text
:
'Tab C'
,
icon
:
'fa fa-fw fa-file-text-o'
,
url
:
path
+
'?tab='
+
TAB_ID_C
,
id
:
TAB_ID_C
,
});
// Set the active tab
let
found
=
false
;
const
selected
=
query
.
tab
||
TAB_ID_B
;
for
(
const
tab
of
tabs
)
{
tab
.
active
=
!
found
&&
selected
===
tab
.
id
;
if
(
tab
.
active
)
{
found
=
true
;
}
}
if
(
!
found
)
{
tabs
[
0
].
active
=
true
;
}
const
node
=
{
text
:
'This is the Page title'
,
img
:
meta
.
info
.
logos
.
large
,
subTitle
:
'subtitle here'
,
url
:
path
,
children
:
tabs
,
};
// Update the page header
onNavChanged
({
node
:
node
,
main
:
node
,
});
}
render
()
{
const
{
path
,
query
,
meta
}
=
this
.
props
;
return
(
<
div
>
QUERY:
<
pre
>
{
JSON
.
stringify
(
query
)
}
</
pre
>
<
br
/>
<
ul
>
<
li
>
<
a
href=
{
path
+
'?x=1'
}
>
111
</
a
>
</
li
>
<
li
>
<
a
href=
{
path
+
'?x=AAA'
}
>
AAA
</
a
>
</
li
>
<
li
>
<
a
href=
{
path
+
'?x=1&y=2&y=3'
}
>
ZZZ
</
a
>
</
li
>
</
ul
>
<
pre
>
{
JSON
.
stringify
(
meta
.
jsonData
)
}
</
pre
>
</
div
>
);
}
}
public/app/plugins/app/example-app/README.md
deleted
100644 → 0
View file @
f6130db0
# Example App - Native Plugin
This is an example app. It has no real use other than making sure external apps are supported.
public/app/plugins/app/example-app/config/ExamplePage1.tsx
deleted
100644 → 0
View file @
f6130db0
// Libraries
import
React
,
{
PureComponent
}
from
'react'
;
// Types
import
{
PluginConfigPageProps
,
AppPluginMeta
}
from
'@grafana/data'
;
import
{
ExampleAppSettings
}
from
'../types'
;
interface
Props
extends
PluginConfigPageProps
<
AppPluginMeta
<
ExampleAppSettings
>>
{}
export
class
ExamplePage1
extends
PureComponent
<
Props
>
{
constructor
(
props
:
Props
)
{
super
(
props
);
}
render
()
{
const
{
query
}
=
this
.
props
;
return
(
<
div
>
11111111111111111111111111111111
<
pre
>
{
JSON
.
stringify
(
query
)
}
</
pre
>
11111111111111111111111111111111
</
div
>
);
}
}
public/app/plugins/app/example-app/config/ExamplePage2.tsx
deleted
100644 → 0
View file @
f6130db0
// Libraries
import
React
,
{
PureComponent
}
from
'react'
;
// Types
import
{
PluginConfigPageProps
,
AppPluginMeta
}
from
'@grafana/data'
;
import
{
ExampleAppSettings
}
from
'../types'
;
interface
Props
extends
PluginConfigPageProps
<
AppPluginMeta
<
ExampleAppSettings
>>
{}
export
class
ExamplePage2
extends
PureComponent
<
Props
>
{
constructor
(
props
:
Props
)
{
super
(
props
);
}
render
()
{
const
{
query
}
=
this
.
props
;
return
(
<
div
>
22222222222222222222222222222222
<
pre
>
{
JSON
.
stringify
(
query
)
}
</
pre
>
22222222222222222222222222222222
</
div
>
);
}
}
public/app/plugins/app/example-app/dashboards/stats.json
deleted
100644 → 0
View file @
f6130db0
{
"__inputs"
:
[],
"__requires"
:
[
{
"type"
:
"grafana"
,
"id"
:
"grafana"
,
"name"
:
"Grafana"
,
"version"
:
"6.2.0-pre"
},
{
"type"
:
"panel"
,
"id"
:
"singlestat2"
,
"name"
:
"Singlestat (react)"
,
"version"
:
""
}
],
"annotations"
:
{
"list"
:
[
{
"builtIn"
:
1
,
"datasource"
:
"-- Grafana --"
,
"enable"
:
true
,
"hide"
:
true
,
"iconColor"
:
"rgba(0, 211, 255, 1)"
,
"name"
:
"Annotations & Alerts"
,
"type"
:
"dashboard"
}
]
},
"editable"
:
true
,
"gnetId"
:
null
,
"graphTooltip"
:
0
,
"id"
:
null
,
"links"
:
[],
"panels"
:
[
{
"gridPos"
:
{
"h"
:
4
,
"w"
:
24
,
"x"
:
0
,
"y"
:
0
},
"id"
:
2
,
"options"
:
{
"orientation"
:
"auto"
,
"sparkline"
:
{
"fillColor"
:
"rgba(31, 118, 189, 0.18)"
,
"full"
:
false
,
"lineColor"
:
"rgb(31, 120, 193)"
,
"show"
:
true
},
"thresholds"
:
[
{
"color"
:
"green"
,
"index"
:
0
,
"value"
:
null
},
{
"color"
:
"red"
,
"index"
:
1
,
"value"
:
80
}
],
"valueMappings"
:
[],
"valueOptions"
:
{
"decimals"
:
null
,
"prefix"
:
""
,
"stat"
:
"mean"
,
"suffix"
:
""
,
"unit"
:
"none"
}
},
"pluginVersion"
:
"6.2.0-pre"
,
"targets"
:
[
{
"refId"
:
"A"
,
"scenarioId"
:
"random_walk_table"
,
"stringInput"
:
""
},
{
"refId"
:
"B"
,
"scenarioId"
:
"random_walk_table"
,
"stringInput"
:
""
}
],
"timeFrom"
:
null
,
"timeShift"
:
null
,
"title"
:
"Panel Title"
,
"type"
:
"singlestat2"
}
],
"schemaVersion"
:
18
,
"style"
:
"dark"
,
"tags"
:
[],
"templating"
:
{
"list"
:
[]
},
"time"
:
{
"from"
:
"now-6h"
,
"to"
:
"now"
},
"timepicker"
:
{
"refresh_intervals"
:
[
"5s"
,
"10s"
,
"30s"
,
"1m"
,
"5m"
,
"15m"
,
"30m"
,
"1h"
,
"2h"
,
"1d"
],
"time_options"
:
[
"5m"
,
"15m"
,
"1h"
,
"6h"
,
"12h"
,
"24h"
,
"2d"
,
"7d"
,
"30d"
]
},
"timezone"
:
""
,
"title"
:
"stats"
,
"uid"
:
"YeBxHjzWz"
,
"version"
:
1
}
public/app/plugins/app/example-app/dashboards/streaming.json
deleted
100644 → 0
View file @
f6130db0
{
"__inputs"
:
[],
"__requires"
:
[
{
"type"
:
"grafana"
,
"id"
:
"grafana"
,
"name"
:
"Grafana"
,
"version"
:
"6.2.0-pre"
},
{
"type"
:
"panel"
,
"id"
:
"graph2"
,
"name"
:
"React Graph"
,
"version"
:
""
}
],
"annotations"
:
{
"list"
:
[
{
"builtIn"
:
1
,
"datasource"
:
"-- Grafana --"
,
"enable"
:
true
,
"hide"
:
true
,
"iconColor"
:
"rgba(0, 211, 255, 1)"
,
"name"
:
"Annotations & Alerts"
,
"type"
:
"dashboard"
}
]
},
"editable"
:
true
,
"gnetId"
:
null
,
"graphTooltip"
:
0
,
"id"
:
null
,
"links"
:
[],
"panels"
:
[
{
"description"
:
""
,
"gridPos"
:
{
"h"
:
6
,
"w"
:
24
,
"x"
:
0
,
"y"
:
0
},
"id"
:
2
,
"links"
:
[],
"targets"
:
[
{
"refId"
:
"A"
,
"scenarioId"
:
"streaming_client"
,
"stream"
:
{
"noise"
:
10
,
"speed"
:
100
,
"spread"
:
20
,
"type"
:
"signal"
},
"stringInput"
:
""
}
],
"timeFrom"
:
null
,
"timeShift"
:
null
,
"title"
:
"Simple dummy streaming example"
,
"type"
:
"graph2"
}
],
"schemaVersion"
:
18
,
"style"
:
"dark"
,
"tags"
:
[],
"templating"
:
{
"list"
:
[]
},
"time"
:
{
"from"
:
"now-1m"
,
"to"
:
"now"
},
"timepicker"
:
{
"refresh_intervals"
:
[
"5s"
,
"10s"
,
"30s"
,
"1m"
,
"5m"
,
"15m"
,
"30m"
,
"1h"
,
"2h"
,
"1d"
],
"time_options"
:
[
"5m"
,
"15m"
,
"1h"
,
"6h"
,
"12h"
,
"24h"
,
"2d"
,
"7d"
,
"30d"
]
},
"timezone"
:
""
,
"title"
:
"simple streaming"
,
"uid"
:
"TbbEZjzWz"
,
"version"
:
1
}
public/app/plugins/app/example-app/img/logo.png
deleted
100644 → 0
View file @
f6130db0
41.7 KB
public/app/plugins/app/example-app/legacy/angular_example_page.html
deleted
100644 → 0
View file @
f6130db0
<h3
class=
"page-heading"
>
Example Page
</h3>
<p>
this is in angular
</p>
public/app/plugins/app/example-app/legacy/angular_example_page.ts
deleted
100644 → 0
View file @
f6130db0
import
{
GrafanaRootScope
}
from
'app/routes/GrafanaCtrl'
;
export
class
AngularExamplePageCtrl
{
static
templateUrl
=
'legacy/angular_example_page.html'
;
/** @ngInject */
constructor
(
$scope
:
any
,
$rootScope
:
GrafanaRootScope
)
{
console
.
log
(
'AngularExamplePageCtrl:'
,
this
);
}
}
public/app/plugins/app/example-app/legacy/config.html
deleted
100644 → 0
View file @
f6130db0
<h2>
Example Application
</h2>
<p>
Angular based config:
</p>
<div
class=
"gf-form"
>
<div
class=
"gf-form-group"
>
<div
class=
"gf-form-inline"
>
<div
class=
"gf-form"
>
<span
class=
"gf-form-label"
>
json Data property
</span>
<input
type=
"text"
class=
"gf-form-input"
ng-model=
"ctrl.appModel.jsonData.customText"
>
</div>
<div
class=
"gf-form"
>
<gf-form-checkbox
class=
"gf-form"
label=
"Custom Value"
checked=
"ctrl.appModel.jsonData.customCheckbox"
switch-class=
"max-width-6"
></gf-form-checkbox>
</div>
</div>
</div>
</div>
public/app/plugins/app/example-app/legacy/config.ts
deleted
100644 → 0
View file @
f6130db0
import
{
PluginMeta
}
from
'@grafana/data'
;
export
class
ExampleConfigCtrl
{
static
templateUrl
=
'legacy/config.html'
;
appEditCtrl
:
any
;
appModel
:
PluginMeta
;
/** @ngInject */
constructor
(
$scope
:
any
,
$injector
:
any
)
{
this
.
appEditCtrl
.
setPostUpdateHook
(
this
.
postUpdate
.
bind
(
this
));
// Make sure it has a JSON Data spot
if
(
!
this
.
appModel
)
{
this
.
appModel
=
{}
as
PluginMeta
;
}
// Required until we get the types sorted on appModel :(
const
appModel
=
this
.
appModel
as
any
;
if
(
!
appModel
.
jsonData
)
{
appModel
.
jsonData
=
{};
}
console
.
log
(
'ExampleConfigCtrl'
,
this
);
}
postUpdate
()
{
if
(
!
this
.
appModel
.
enabled
)
{
console
.
log
(
'Not enabled...'
);
return
;
}
// TODO, can do stuff after update
console
.
log
(
'Post Update:'
,
this
);
}
}
public/app/plugins/app/example-app/module.ts
deleted
100644 → 0
View file @
f6130db0
// Angular pages
import
{
ExampleConfigCtrl
}
from
'./legacy/config'
;
import
{
AngularExamplePageCtrl
}
from
'./legacy/angular_example_page'
;
import
{
AppPlugin
}
from
'@grafana/data'
;
import
{
ExamplePage1
}
from
'./config/ExamplePage1'
;
import
{
ExamplePage2
}
from
'./config/ExamplePage2'
;
import
{
ExampleRootPage
}
from
'./ExampleRootPage'
;
import
{
ExampleAppSettings
}
from
'./types'
;
// Legacy exports just for testing
export
{
ExampleConfigCtrl
as
ConfigCtrl
,
AngularExamplePageCtrl
,
// Must match `pages.component` in plugin.json
};
export
const
plugin
=
new
AppPlugin
<
ExampleAppSettings
>
()
.
setRootPage
(
ExampleRootPage
)
.
addConfigPage
({
title
:
'Page 1'
,
icon
:
'fa fa-info'
,
body
:
ExamplePage1
,
id
:
'page1'
,
})
.
addConfigPage
({
title
:
'Page 2'
,
icon
:
'fa fa-user'
,
body
:
ExamplePage2
,
id
:
'page2'
,
});
public/app/plugins/app/example-app/plugin.json
deleted
100644 → 0
View file @
f6130db0
{
"type"
:
"app"
,
"name"
:
"Example App"
,
"id"
:
"example-app"
,
"state"
:
"alpha"
,
"info"
:
{
"author"
:
{
"name"
:
"Grafana Project"
,
"url"
:
"https://grafana.com"
},
"logos"
:
{
"small"
:
"img/logo.png"
,
"large"
:
"img/logo.png"
}
},
"includes"
:
[
{
"type"
:
"page"
,
"name"
:
"Angular Page"
,
"component"
:
"AngularExamplePageCtrl"
,
"role"
:
"Viewer"
,
"addToNav"
:
true
,
"defaultNav"
:
true
},
{
"type"
:
"dashboard"
,
"name"
:
"Streaming Example"
,
"path"
:
"dashboards/streaming.json"
},
{
"type"
:
"dashboard"
,
"name"
:
"Lots of Stats"
,
"path"
:
"dashboards/stats.json"
},
{
"type"
:
"panel"
,
"name"
:
"Anything -- just display?"
}
]
}
public/app/plugins/app/example-app/types.ts
deleted
100644 → 0
View file @
f6130db0
export
interface
ExampleAppSettings
{
customText
?:
string
;
customCheckbox
?:
boolean
;
}
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