Merge remote-tracking branch 'origin/master' into feature/status_page-1556
commit
b84d23e7e0
|
@ -2,6 +2,8 @@
|
|||
|
||||
### Bug Fixes
|
||||
1. [#1512](https://github.com/influxdata/chronograf/pull/1512): Prevent legend from flowing over window bottom bound
|
||||
1. [#1600](https://github.com/influxdata/chronograf/pull/1600): Prevent Kapacitor configurations from having the same name
|
||||
1. [#1600](https://github.com/influxdata/chronograf/pull/1600): Limit Kapacitor configuration names to 33 characters to fix display bug
|
||||
|
||||
### Features
|
||||
1. [#1512](https://github.com/influxdata/chronograf/pull/1512): Synchronize vertical crosshair at same time across all graphs in a dashboard
|
||||
|
@ -10,6 +12,8 @@
|
|||
### UI Improvements
|
||||
1. [#1512](https://github.com/influxdata/chronograf/pull/1512): When dashboard time range is changed, reset graphs that are zoomed in
|
||||
1. [#1599](https://github.com/influxdata/chronograf/pull/1599): Bar graph option added to dashboard
|
||||
1. [#1600](https://github.com/influxdata/chronograf/pull/1600): Redesign source management table to be more intuitive
|
||||
1. [#1600](https://github.com/influxdata/chronograf/pull/1600): Redesign Line + Single Stat cells to appear more like a sparkline, and improve legibility
|
||||
|
||||
## v1.3.2.1 [2017-06-06]
|
||||
|
||||
|
|
|
@ -99,7 +99,7 @@ const DashboardsPage = React.createClass({
|
|||
<tbody>
|
||||
{dashboards.map(dashboard =>
|
||||
<tr key={dashboard.id} className="">
|
||||
<td className="monotype">
|
||||
<td>
|
||||
<Link
|
||||
to={`${dashboardLink}/dashboards/${dashboard.id}`}
|
||||
>
|
||||
|
|
|
@ -51,7 +51,7 @@ const Header = React.createClass({
|
|||
<div className="page-header__right">
|
||||
<GraphTips />
|
||||
<SourceIndicator sourceName={this.context.source.name} />
|
||||
<div className="btn btn-sm btn-info" onClick={showWriteForm}>
|
||||
<div className="btn btn-sm btn-default" onClick={showWriteForm}>
|
||||
<span className="icon pencil" />
|
||||
Write Data
|
||||
</div>
|
||||
|
|
|
@ -298,12 +298,12 @@ Dygraph.Plugins.Crosshair = (function() {
|
|||
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, height)
|
||||
gradient.addColorStop(0, 'rgba(255, 255, 255, 0.0)')
|
||||
gradient.addColorStop(0.2, 'rgba(255, 255, 255, 1.0)')
|
||||
gradient.addColorStop(0.8, 'rgba(255, 255, 255, 1.0)')
|
||||
gradient.addColorStop(0.11, 'rgba(255, 255, 255, 1.0)')
|
||||
gradient.addColorStop(0.89, 'rgba(255, 255, 255, 1.0)')
|
||||
gradient.addColorStop(1, 'rgba(255, 255, 255, 0.0)')
|
||||
|
||||
ctx.strokeStyle = gradient
|
||||
ctx.lineWidth = 2
|
||||
ctx.lineWidth = 1.5
|
||||
|
||||
// If graphs have different time ranges, it's possible to select a point on
|
||||
// one graph that doesn't exist in another, resulting in an exception.
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import {Link} from 'react-router'
|
||||
import _ from 'lodash'
|
||||
|
||||
import shallowCompare from 'react-addons-shallow-compare'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import _ from 'lodash'
|
||||
import {HOSTS_TABLE} from 'src/hosts/constants/tableSizing'
|
||||
|
||||
const {arrayOf, bool, number, shape, string} = PropTypes
|
||||
|
||||
|
@ -101,6 +101,7 @@ const HostsTable = React.createClass({
|
|||
sortDirection
|
||||
)
|
||||
const hostCount = sortedHosts.length
|
||||
const {colName, colStatus, colCPU, colLoad} = HOSTS_TABLE
|
||||
|
||||
let hostsTitle
|
||||
|
||||
|
@ -128,27 +129,28 @@ const HostsTable = React.createClass({
|
|||
<th
|
||||
onClick={() => this.updateSort('name')}
|
||||
className={this.sortableClasses('name')}
|
||||
style={{width: colName}}
|
||||
>
|
||||
Host
|
||||
</th>
|
||||
<th
|
||||
onClick={() => this.updateSort('deltaUptime')}
|
||||
className={this.sortableClasses('deltaUptime')}
|
||||
style={{width: '74px'}}
|
||||
style={{width: colStatus}}
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
<th
|
||||
onClick={() => this.updateSort('cpu')}
|
||||
className={this.sortableClasses('cpu')}
|
||||
style={{width: '70px'}}
|
||||
style={{width: colCPU}}
|
||||
>
|
||||
CPU
|
||||
</th>
|
||||
<th
|
||||
onClick={() => this.updateSort('load')}
|
||||
className={this.sortableClasses('load')}
|
||||
style={{width: '68px'}}
|
||||
style={{width: colLoad}}
|
||||
>
|
||||
Load
|
||||
</th>
|
||||
|
@ -195,13 +197,14 @@ const HostRow = React.createClass({
|
|||
render() {
|
||||
const {host, source} = this.props
|
||||
const {name, cpu, load, apps = []} = host
|
||||
const {colName, colStatus, colCPU, colLoad} = HOSTS_TABLE
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td className="monotype">
|
||||
<td style={{width: colName}}>
|
||||
<Link to={`/sources/${source.id}/hosts/${name}`}>{name}</Link>
|
||||
</td>
|
||||
<td style={{width: '74px'}}>
|
||||
<td style={{width: colStatus}}>
|
||||
<div
|
||||
className={classnames(
|
||||
'table-dot',
|
||||
|
@ -209,13 +212,13 @@ const HostRow = React.createClass({
|
|||
)}
|
||||
/>
|
||||
</td>
|
||||
<td className="monotype" style={{width: '70px'}}>
|
||||
<td style={{width: colCPU}} className="monotype">
|
||||
{isNaN(cpu) ? 'N/A' : `${cpu.toFixed(2)}%`}
|
||||
</td>
|
||||
<td className="monotype" style={{width: '68px'}}>
|
||||
<td style={{width: colLoad}} className="monotype">
|
||||
{isNaN(load) ? 'N/A' : `${load.toFixed(2)}`}
|
||||
</td>
|
||||
<td className="monotype">
|
||||
<td>
|
||||
{apps.map((app, index) => {
|
||||
return (
|
||||
<span key={app}>
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
export const HOSTS_TABLE = {
|
||||
colName: '40%',
|
||||
colStatus: '74px',
|
||||
colCPU: '70px',
|
||||
colLoad: '68px',
|
||||
}
|
|
@ -52,6 +52,7 @@ class KapacitorForm extends Component {
|
|||
value={name}
|
||||
onChange={onInputChange}
|
||||
spellCheck="false"
|
||||
maxLength="33"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
|
|
|
@ -71,6 +71,15 @@ class KapacitorPage extends Component {
|
|||
const {addFlashMessage, source, params, router} = this.props
|
||||
const {kapacitor} = this.state
|
||||
|
||||
const kapNames = source.kapacitors.map(k => k.name)
|
||||
if (kapNames.includes(kapacitor.name)) {
|
||||
addFlashMessage({
|
||||
type: 'error',
|
||||
text: `There is already a Kapacitor configuration named "${kapacitor.name}"`,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (params.id) {
|
||||
updateKapacitor(kapacitor)
|
||||
.then(({data}) => {
|
||||
|
@ -142,7 +151,7 @@ class KapacitorPage extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
const {func, shape, string} = PropTypes
|
||||
const {array, func, shape, string} = PropTypes
|
||||
|
||||
KapacitorPage.propTypes = {
|
||||
addFlashMessage: func,
|
||||
|
@ -155,6 +164,7 @@ KapacitorPage.propTypes = {
|
|||
source: shape({
|
||||
id: string.isRequired,
|
||||
url: string.isRequired,
|
||||
kapacitors: array.isRequired,
|
||||
}),
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,8 @@ import lastValues from 'shared/parsing/lastValues'
|
|||
|
||||
const {array, arrayOf, bool, func, number, shape, string} = PropTypes
|
||||
|
||||
const SMALL_CELL_HEIGHT = 1
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'LineGraph',
|
||||
propTypes: {
|
||||
|
@ -37,6 +39,7 @@ export default React.createClass({
|
|||
}),
|
||||
isInDataExplorer: bool,
|
||||
synchronizer: func,
|
||||
cellHeight: number,
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
|
@ -91,6 +94,7 @@ export default React.createClass({
|
|||
ruleValues,
|
||||
synchronizer,
|
||||
timeRange,
|
||||
cellHeight,
|
||||
} = this.props
|
||||
const {labels, timeSeries, dygraphSeries} = this._timeSeries
|
||||
|
||||
|
@ -112,7 +116,7 @@ export default React.createClass({
|
|||
title,
|
||||
rightGap: 0,
|
||||
yRangePad: 10,
|
||||
axisLabelWidth: 38,
|
||||
axisLabelWidth: 60,
|
||||
drawAxesAtZero: true,
|
||||
underlayCallback,
|
||||
ylabel: _.get(queries, ['0', 'label'], ''),
|
||||
|
@ -120,6 +124,32 @@ export default React.createClass({
|
|||
...displayOptions,
|
||||
}
|
||||
|
||||
const singleStatOptions = {
|
||||
labels,
|
||||
connectSeparatedPoints: true,
|
||||
labelsKMB: true,
|
||||
axes: {
|
||||
x: {
|
||||
drawGrid: false,
|
||||
drawAxis: false,
|
||||
},
|
||||
y: {
|
||||
drawGrid: false,
|
||||
drawAxis: false,
|
||||
},
|
||||
},
|
||||
title,
|
||||
rightGap: 0,
|
||||
strokeWidth: 1.5,
|
||||
drawAxesAtZero: true,
|
||||
underlayCallback,
|
||||
...displayOptions,
|
||||
highlightSeriesOpts: {
|
||||
strokeWidth: 1.5,
|
||||
},
|
||||
}
|
||||
const singleStatLineColor = ['#7A65F2']
|
||||
|
||||
let roundedValue
|
||||
if (showSingleStat) {
|
||||
const lastValue = lastValues(data)[1]
|
||||
|
@ -138,12 +168,14 @@ export default React.createClass({
|
|||
{isRefreshing ? this.renderSpinner() : null}
|
||||
<Dygraph
|
||||
containerStyle={{width: '100%', height: '100%'}}
|
||||
overrideLineColors={overrideLineColors}
|
||||
isGraphFilled={isGraphFilled}
|
||||
overrideLineColors={
|
||||
showSingleStat ? singleStatLineColor : overrideLineColors
|
||||
}
|
||||
isGraphFilled={showSingleStat ? false : isGraphFilled}
|
||||
isBarGraph={isBarGraph}
|
||||
timeSeries={timeSeries}
|
||||
labels={labels}
|
||||
options={options}
|
||||
options={showSingleStat ? singleStatOptions : options}
|
||||
dygraphSeries={dygraphSeries}
|
||||
ranges={ranges || this.getRanges()}
|
||||
ruleValues={ruleValues}
|
||||
|
@ -151,8 +183,14 @@ export default React.createClass({
|
|||
timeRange={timeRange}
|
||||
/>
|
||||
{showSingleStat
|
||||
? <div className="graph-single-stat single-stat">
|
||||
<span className="single-stat--value">{roundedValue}</span>
|
||||
? <div className="single-stat single-stat-line">
|
||||
<span
|
||||
className={classnames('single-stat--value', {
|
||||
'single-stat--small': cellHeight === SMALL_CELL_HEIGHT,
|
||||
})}
|
||||
>
|
||||
<span className="single-stat--shadow">{roundedValue}</span>
|
||||
</span>
|
||||
</div>
|
||||
: null}
|
||||
</div>
|
||||
|
|
|
@ -8,4 +8,5 @@
|
|||
{defaultGroupBy: '30m', seconds: 172800, inputValue: 'Past 2 days', lower: 'now() - 2d', upper: null, menuOption: 'Past 2 days'},
|
||||
{defaultGroupBy: '1h', seconds: 604800, inputValue: 'Past 7 days', lower: 'now() - 7d', upper: null, menuOption: 'Past 7 days'},
|
||||
{defaultGroupBy: '6h', seconds: 2592000, inputValue: 'Past 30 days', lower: 'now() - 30d', upper: null, menuOption: 'Past 30 days'},
|
||||
{defaultGroupBy: '12h', seconds: 7776000, inputValue: 'Past 90 days', lower: 'now() - 90d', upper: null, menuOption: 'Past 90 days'},
|
||||
]
|
||||
|
|
|
@ -9,6 +9,8 @@ import {
|
|||
NavListItem,
|
||||
} from 'src/side_nav/components/NavItems'
|
||||
|
||||
import {DEFAULT_HOME_PAGE} from 'shared/constants'
|
||||
|
||||
const {bool, shape, string} = PropTypes
|
||||
|
||||
const SideNav = React.createClass({
|
||||
|
@ -39,7 +41,7 @@ const SideNav = React.createClass({
|
|||
? null
|
||||
: <NavBar location={location}>
|
||||
<div className="sidebar__logo">
|
||||
<Link to={`${sourcePrefix}/status`}>
|
||||
<Link to={`${sourcePrefix}/${DEFAULT_HOME_PAGE}`}>
|
||||
<span className="icon cubo-uniform" />
|
||||
</Link>
|
||||
</div>
|
||||
|
|
|
@ -2,6 +2,7 @@ import React, {PropTypes} from 'react'
|
|||
import {Link, withRouter} from 'react-router'
|
||||
|
||||
import Dropdown from 'shared/components/Dropdown'
|
||||
import QuestionMarkTooltip from 'shared/components/QuestionMarkTooltip'
|
||||
|
||||
const kapacitorDropdown = (
|
||||
kapacitors,
|
||||
|
@ -12,7 +13,12 @@ const kapacitorDropdown = (
|
|||
) => {
|
||||
if (!kapacitors || kapacitors.length === 0) {
|
||||
return (
|
||||
<Link to={`/sources/${source.id}/kapacitors/new`}>Add Kapacitor</Link>
|
||||
<Link
|
||||
to={`/sources/${source.id}/kapacitors/new`}
|
||||
className="btn btn-xs btn-default"
|
||||
>
|
||||
Add Config
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
const kapacitorItems = kapacitors.map(k => {
|
||||
|
@ -35,7 +41,7 @@ const kapacitorDropdown = (
|
|||
return (
|
||||
<Dropdown
|
||||
className="dropdown-260"
|
||||
buttonColor="btn-default"
|
||||
buttonColor="btn-primary"
|
||||
buttonSize="btn-xs"
|
||||
items={kapacitorItems}
|
||||
onChoose={item => setActiveKapacitor(item.kapacitor)}
|
||||
|
@ -90,27 +96,59 @@ const InfluxTable = ({
|
|||
<table className="table v-center margin-bottom-zero table-highlight">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Host</th>
|
||||
<th>Kapacitor</th>
|
||||
<th className="source-table--connect-col" />
|
||||
<th>Source Name & Host</th>
|
||||
<th className="text-right" />
|
||||
<th>
|
||||
Active Kapacitor{' '}
|
||||
<QuestionMarkTooltip
|
||||
tipID="kapacitor-node-helper"
|
||||
tipContent="Kapacitor Configurations are scoped per InfluxDB Source. Only one can be active at a time"
|
||||
/>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sources.map(s => {
|
||||
return (
|
||||
<tr key={s.id}>
|
||||
<tr
|
||||
key={s.id}
|
||||
className={s.id === source.id ? 'highlight' : null}
|
||||
>
|
||||
<td>
|
||||
<Link to={`${location.pathname}/${s.id}/edit`}>
|
||||
{s.name}
|
||||
</Link>
|
||||
{' '}
|
||||
{s.default
|
||||
? <span className="default-source-label">Default</span>
|
||||
: null}
|
||||
{s.id === source.id
|
||||
? <div className="btn btn-success btn-xs source-table--connect">
|
||||
Connected
|
||||
</div>
|
||||
: <Link
|
||||
className="btn btn-default btn-xs source-table--connect"
|
||||
to={`/sources/${s.id}/hosts`}
|
||||
>
|
||||
Connect
|
||||
</Link>}
|
||||
</td>
|
||||
<td className="monotype">{s.url}</td>
|
||||
<td>
|
||||
<h5 className="margin-zero">
|
||||
<Link
|
||||
to={`${location.pathname}/${s.id}/edit`}
|
||||
className={s.id === source.id ? 'link-success' : null}
|
||||
>
|
||||
<strong>{s.name}</strong>
|
||||
{s.default ? ' (Default)' : null}
|
||||
</Link>
|
||||
</h5>
|
||||
<span>{s.url}</span>
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<a
|
||||
className="btn btn-xs btn-danger table--show-on-row-hover"
|
||||
href="#"
|
||||
onClick={() => handleDeleteSource(s)}
|
||||
>
|
||||
Delete Source
|
||||
</a>
|
||||
</td>
|
||||
<td className="source-table--kapacitor">
|
||||
{kapacitorDropdown(
|
||||
s.kapacitors,
|
||||
s,
|
||||
|
@ -119,24 +157,6 @@ const InfluxTable = ({
|
|||
handleDeleteKapacitor
|
||||
)}
|
||||
</td>
|
||||
<td className="text-right">
|
||||
{s.id === source.id
|
||||
? <span className="currently-connected-source">
|
||||
<span className="icon checkmark" /> Connected
|
||||
</span>
|
||||
: <Link
|
||||
className="btn btn-success btn-xs"
|
||||
to={`/sources/${s.id}/hosts`}
|
||||
>
|
||||
Connect
|
||||
</Link>}
|
||||
<button
|
||||
className="btn btn-danger btn-xs btn-square"
|
||||
onClick={() => handleDeleteSource(s)}
|
||||
>
|
||||
<span className="icon trash" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
|
|
|
@ -3,7 +3,7 @@ import timeRanges from 'hson!shared/data/timeRanges.hson'
|
|||
|
||||
import * as actionTypes from 'src/status/constants/actionTypes'
|
||||
|
||||
const {lower, upper} = timeRanges.find(tr => tr.lower === 'now() - 30d')
|
||||
const {lower, upper} = timeRanges.find(tr => tr.lower === 'now() - 90d')
|
||||
|
||||
const initialState = {
|
||||
autoRefresh: AUTOREFRESH_DEFAULT,
|
||||
|
|
|
@ -128,10 +128,10 @@
|
|||
.graph--hasYLabel {
|
||||
|
||||
.dygraph-axis-label-y {
|
||||
padding: 0 1px 0 10px !important;
|
||||
padding: 0 1px 0 16px !important;
|
||||
}
|
||||
.dygraph-axis-label-y2 {
|
||||
padding: 0 10px 0 1px !important;
|
||||
padding: 0 16px 0 1px !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -153,13 +153,37 @@
|
|||
top: calc(50% - 15px);
|
||||
left: 50%;
|
||||
transform: translate(-50%,-50%);
|
||||
width: calc(100% - 32px);
|
||||
// overflow: hidden;
|
||||
text-align: center;
|
||||
// text-overflow: ellipsis;
|
||||
font-size: 54px;
|
||||
line-height: 54px;
|
||||
font-weight: 300;
|
||||
color: $c-pool;
|
||||
color: $c-laser;
|
||||
z-index: 1;
|
||||
|
||||
&.single-stat--small {
|
||||
font-size: 38px;
|
||||
line-height: 38px;
|
||||
font-weight: 400;
|
||||
font-size: 34px;
|
||||
line-height: 34px;
|
||||
}
|
||||
}
|
||||
.single-stat--shadow {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
.single-stat--shadow:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 110%;
|
||||
height: 0;
|
||||
transform: translate(-50%,-50%);
|
||||
box-shadow: fade-out($g2-kevlar, 0.3) 0 0 50px 30px;
|
||||
z-index: -1;
|
||||
}
|
||||
.single-stat--small .single-stat--shadow:after {
|
||||
box-shadow: fade-out($g2-kevlar, 0.3) 0 0 30px 10px;
|
||||
}
|
||||
|
|
|
@ -167,6 +167,11 @@ $table-tab-scrollbar-height: 6px;
|
|||
border-radius: 0 $radius-small $radius-small $radius-small;
|
||||
}
|
||||
|
||||
|
||||
.table > tbody > tr.highlight,
|
||||
.table.table-highlight > tbody > tr.highlight {
|
||||
background-color: $g4-onyx;
|
||||
}
|
||||
/*
|
||||
Responsive Tables
|
||||
----------------------------------------------
|
||||
|
|
|
@ -149,7 +149,7 @@ pre.admin-table--query {
|
|||
font-size: 16px;
|
||||
font-family: $code-font;
|
||||
padding-left: 6px;
|
||||
width: auto;
|
||||
@include no-user-select();
|
||||
}
|
||||
}
|
||||
.db-manager-header--edit {
|
||||
|
|
|
@ -204,6 +204,7 @@ input.form-control.dash-graph--name-edit {
|
|||
transition-property: all;
|
||||
|
||||
> li {
|
||||
@include no-user-select;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
|
@ -241,7 +242,7 @@ input.form-control.dash-graph--name-edit {
|
|||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: $g6-smoke;
|
||||
background-color: $g7-graphite;
|
||||
color: $g20-white;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,6 @@ $rule-builder--radius-lg: 5px;
|
|||
align-items: stretch;
|
||||
}
|
||||
.rule-builder p {
|
||||
width: auto;
|
||||
margin: 0 ($rule-builder--padding-sm - 2px);
|
||||
font-weight: 600;
|
||||
color: $g15-platinum;
|
||||
|
|
|
@ -54,7 +54,6 @@ $overlay-z: 100;
|
|||
margin: 0 0 0 5px;
|
||||
}
|
||||
p {
|
||||
width: auto;
|
||||
font-weight: 600;
|
||||
color: $g13-mist;
|
||||
margin: 0 6px 0 0;
|
||||
|
@ -63,7 +62,6 @@ $overlay-z: 100;
|
|||
}
|
||||
}
|
||||
.overlay--graph-name {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
font-weight: 400;
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -158,6 +158,17 @@ br {
|
|||
@include gradient-v($g2-kevlar, $g0-obsidian);
|
||||
}
|
||||
|
||||
.source-table--connect {
|
||||
width: 74px;
|
||||
}
|
||||
.source-table--connect-col {
|
||||
width: 90px;
|
||||
}
|
||||
.source-table--kapacitor {
|
||||
border-left: 2px solid $g5-pepper;
|
||||
width: 278px;
|
||||
}
|
||||
|
||||
/*
|
||||
Styles for the Status Dashboard
|
||||
-----------------------------------------------------------------------------
|
||||
|
|
Loading…
Reference in New Issue