mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
grid: minor progress
This commit is contained in:
parent
a867dc069b
commit
207773e07e
11
public/app/features/dashboard/PanelModel.ts
Normal file
11
public/app/features/dashboard/PanelModel.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
export interface PanelModel {
|
||||||
|
id: number;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
@ -1,146 +1,148 @@
|
|||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import angular from 'angular';
|
|
||||||
|
|
||||||
import coreModule from 'app/core/core_module';
|
import coreModule from 'app/core/core_module';
|
||||||
|
import {PanelContainer} from './dashgrid/PanelContainer';
|
||||||
|
import {DashboardModel} from './model';
|
||||||
|
import {PanelModel} from './PanelModel';
|
||||||
|
|
||||||
export class DashboardCtrl {
|
export class DashboardCtrl implements PanelContainer {
|
||||||
|
dashboard: DashboardModel;
|
||||||
|
dashboardViewState: any;
|
||||||
|
loadedFallbackDashboard: boolean;
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor(
|
constructor(
|
||||||
private $scope,
|
private $scope,
|
||||||
$rootScope,
|
private $rootScope,
|
||||||
keybindingSrv,
|
private keybindingSrv,
|
||||||
timeSrv,
|
private timeSrv,
|
||||||
variableSrv,
|
private variableSrv,
|
||||||
alertingSrv,
|
private alertingSrv,
|
||||||
dashboardSrv,
|
private dashboardSrv,
|
||||||
unsavedChangesSrv,
|
private unsavedChangesSrv,
|
||||||
dynamicDashboardSrv,
|
private dynamicDashboardSrv,
|
||||||
dashboardViewStateSrv,
|
private dashboardViewStateSrv,
|
||||||
panelLoader,
|
private panelLoader) {
|
||||||
contextSrv,
|
// temp hack due to way dashboards are loaded
|
||||||
alertSrv,
|
// can't use controllerAs on route yet
|
||||||
$timeout) {
|
$scope.ctrl = this;
|
||||||
|
|
||||||
$scope.editor = { index: 0 };
|
// funcs called from React component bindings and needs this binding
|
||||||
|
this.getPanelContainer = this.getPanelContainer.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
var resizeEventTimeout;
|
setupDashboard(data) {
|
||||||
|
try {
|
||||||
|
this.setupDashboardInternal(data);
|
||||||
|
} catch (err) {
|
||||||
|
this.onInitFailed(err, 'Dashboard init failed', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$scope.setupDashboard = function(data) {
|
setupDashboardInternal(data) {
|
||||||
try {
|
const dashboard = this.dashboardSrv.create(data.dashboard, data.meta);
|
||||||
$scope.setupDashboardInternal(data);
|
this.dashboardSrv.setCurrent(dashboard);
|
||||||
} catch (err) {
|
|
||||||
$scope.onInitFailed(err, 'Dashboard init failed', true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.setupDashboardInternal = function(data) {
|
// init services
|
||||||
var dashboard = dashboardSrv.create(data.dashboard, data.meta);
|
this.timeSrv.init(dashboard);
|
||||||
dashboardSrv.setCurrent(dashboard);
|
this.alertingSrv.init(dashboard, data.alerts);
|
||||||
|
|
||||||
// init services
|
// template values service needs to initialize completely before
|
||||||
timeSrv.init(dashboard);
|
// the rest of the dashboard can load
|
||||||
alertingSrv.init(dashboard, data.alerts);
|
this.variableSrv.init(dashboard)
|
||||||
|
// template values failes are non fatal
|
||||||
|
.catch(this.onInitFailed.bind(this, 'Templating init failed', false))
|
||||||
|
// continue
|
||||||
|
.finally(() => {
|
||||||
|
this.dashboard = dashboard;
|
||||||
|
|
||||||
// template values service needs to initialize completely before
|
this.dynamicDashboardSrv.init(dashboard);
|
||||||
// the rest of the dashboard can load
|
this.dynamicDashboardSrv.process();
|
||||||
variableSrv.init(dashboard)
|
|
||||||
// template values failes are non fatal
|
|
||||||
.catch($scope.onInitFailed.bind(this, 'Templating init failed', false))
|
|
||||||
// continue
|
|
||||||
.finally(function() {
|
|
||||||
dynamicDashboardSrv.init(dashboard);
|
|
||||||
dynamicDashboardSrv.process();
|
|
||||||
|
|
||||||
unsavedChangesSrv.init(dashboard, $scope);
|
this.unsavedChangesSrv.init(dashboard, this.$scope);
|
||||||
|
|
||||||
$scope.dashboard = dashboard;
|
// TODO refactor ViewStateSrv
|
||||||
$scope.dashboardMeta = dashboard.meta;
|
this.$scope.dashboard = dashboard;
|
||||||
$scope.dashboardViewState = dashboardViewStateSrv.create($scope);
|
this.dashboardViewState = this.dashboardViewStateSrv.create(this.$scope);
|
||||||
|
|
||||||
keybindingSrv.setupDashboardBindings($scope, dashboard);
|
this.keybindingSrv.setupDashboardBindings(this.$scope, dashboard);
|
||||||
|
|
||||||
$scope.dashboard.updateSubmenuVisibility();
|
this.dashboard.updateSubmenuVisibility();
|
||||||
$scope.setWindowTitleAndTheme();
|
this.setWindowTitleAndTheme();
|
||||||
|
|
||||||
$scope.appEvent("dashboard-initialized", $scope.dashboard);
|
this.$scope.appEvent("dashboard-initialized", dashboard);
|
||||||
})
|
})
|
||||||
.catch($scope.onInitFailed.bind(this, 'Dashboard init failed', true));
|
.catch(this.onInitFailed.bind(this, 'Dashboard init failed', true));
|
||||||
};
|
}
|
||||||
|
|
||||||
$scope.onInitFailed = function(msg, fatal, err) {
|
onInitFailed(msg, fatal, err) {
|
||||||
console.log(msg, err);
|
console.log(msg, err);
|
||||||
|
|
||||||
if (err.data && err.data.message) {
|
if (err.data && err.data.message) {
|
||||||
err.message = err.data.message;
|
err.message = err.data.message;
|
||||||
} else if (!err.message) {
|
} else if (!err.message) {
|
||||||
err = {message: err.toString()};
|
err = {message: err.toString()};
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.appEvent("alert-error", [msg, err.message]);
|
this.$scope.appEvent("alert-error", [msg, err.message]);
|
||||||
|
|
||||||
// protect against recursive fallbacks
|
// protect against recursive fallbacks
|
||||||
if (fatal && !$scope.loadedFallbackDashboard) {
|
if (fatal && !this.loadedFallbackDashboard) {
|
||||||
$scope.loadedFallbackDashboard = true;
|
this.loadedFallbackDashboard = true;
|
||||||
$scope.setupDashboard({dashboard: {title: 'Dashboard Init failed'}});
|
this.setupDashboard({dashboard: {title: 'Dashboard Init failed'}});
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
$scope.templateVariableUpdated = function() {
|
templateVariableUpdated() {
|
||||||
dynamicDashboardSrv.process();
|
this.dynamicDashboardSrv.process();
|
||||||
};
|
}
|
||||||
|
|
||||||
$scope.setWindowTitleAndTheme = function() {
|
setWindowTitleAndTheme() {
|
||||||
window.document.title = config.window_title_prefix + $scope.dashboard.title;
|
window.document.title = config.window_title_prefix + this.dashboard.title;
|
||||||
};
|
}
|
||||||
|
|
||||||
$scope.broadcastRefresh = function() {
|
showJsonEditor(evt, options) {
|
||||||
$rootScope.$broadcast('refresh');
|
var editScope = this.$rootScope.$new();
|
||||||
};
|
editScope.object = options.object;
|
||||||
|
editScope.updateHandler = options.updateHandler;
|
||||||
|
this.$scope.appEvent('show-dash-editor', { src: 'public/app/partials/edit_json.html', scope: editScope });
|
||||||
|
}
|
||||||
|
|
||||||
$scope.addRowDefault = function() {
|
getDashboard() {
|
||||||
$scope.dashboard.addEmptyRow();
|
return this.dashboard;
|
||||||
};
|
}
|
||||||
|
|
||||||
$scope.showJsonEditor = function(evt, options) {
|
getPanelLoader() {
|
||||||
var editScope = $rootScope.$new();
|
return this.panelLoader;
|
||||||
editScope.object = options.object;
|
}
|
||||||
editScope.updateHandler = options.updateHandler;
|
|
||||||
$scope.appEvent('show-dash-editor', { src: 'public/app/partials/edit_json.html', scope: editScope });
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.registerWindowResizeEvent = function() {
|
getPanels() {
|
||||||
angular.element(window).bind('resize', function() {
|
return this.dashboard.panels;
|
||||||
$timeout.cancel(resizeEventTimeout);
|
}
|
||||||
resizeEventTimeout = $timeout(function() { $scope.$broadcast('render'); }, 200);
|
|
||||||
});
|
|
||||||
|
|
||||||
$scope.$on('$destroy', function() {
|
panelPossitionUpdated(panel: PanelModel) {
|
||||||
angular.element(window).unbind('resize');
|
console.log('panel pos updated', panel);
|
||||||
$scope.dashboard.destroy();
|
}
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.timezoneChanged = function() {
|
timezoneChanged() {
|
||||||
$rootScope.$broadcast("refresh");
|
this.$rootScope.$broadcast("refresh");
|
||||||
};
|
}
|
||||||
|
|
||||||
$scope.onFolderChange = function(folder) {
|
onFolderChange(folder) {
|
||||||
$scope.dashboard.folderId = folder.id;
|
this.dashboard.folderId = folder.id;
|
||||||
$scope.dashboard.meta.folderId = folder.id;
|
this.dashboard.meta.folderId = folder.id;
|
||||||
$scope.dashboard.meta.folderTitle= folder.title;
|
this.dashboard.meta.folderTitle= folder.title;
|
||||||
};
|
}
|
||||||
|
|
||||||
$scope.getPanelLoader = function() {
|
getPanelContainer() {
|
||||||
return panelLoader;
|
console.log('DashboardCtrl:getPanelContainer()');
|
||||||
};
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
init(dashboard) {
|
init(dashboard) {
|
||||||
this.$scope.onAppEvent('show-json-editor', this.$scope.showJsonEditor);
|
this.$scope.onAppEvent('show-json-editor', this.$scope.showJsonEditor);
|
||||||
this.$scope.onAppEvent('template-variable-value-updated', this.$scope.templateVariableUpdated);
|
this.$scope.onAppEvent('template-variable-value-updated', this.$scope.templateVariableUpdated);
|
||||||
this.$scope.setupDashboard(dashboard);
|
this.setupDashboard(dashboard);
|
||||||
this.$scope.registerWindowResizeEvent();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import coreModule from 'app/core/core_module';
|
import coreModule from 'app/core/core_module';
|
||||||
import ReactGridLayout from 'react-grid-layout';
|
import ReactGridLayout from 'react-grid-layout';
|
||||||
import {DashboardModel} from '../model';
|
import {CELL_HEIGHT, CELL_VMARGIN} from '../model';
|
||||||
import {DashboardPanel} from './DashboardPanel';
|
import {DashboardPanel} from './DashboardPanel';
|
||||||
import {PanelLoader} from './PanelLoader';
|
import {PanelContainer} from './PanelContainer';
|
||||||
import sizeMe from 'react-sizeme';
|
import sizeMe from 'react-sizeme';
|
||||||
|
|
||||||
const COLUMN_COUNT = 12;
|
const COLUMN_COUNT = 12;
|
||||||
const ROW_HEIGHT = 30;
|
|
||||||
|
|
||||||
function GridWrapper({size, layout, onLayoutChange, children}) {
|
function GridWrapper({size, layout, onLayoutChange, children}) {
|
||||||
if (size.width === 0) {
|
if (size.width === 0) {
|
||||||
@ -23,9 +22,9 @@ function GridWrapper({size, layout, onLayoutChange, children}) {
|
|||||||
isDraggable={true}
|
isDraggable={true}
|
||||||
isResizable={true}
|
isResizable={true}
|
||||||
measureBeforeMount={false}
|
measureBeforeMount={false}
|
||||||
margin={[10, 10]}
|
margin={[CELL_VMARGIN, CELL_VMARGIN]}
|
||||||
cols={COLUMN_COUNT}
|
cols={COLUMN_COUNT}
|
||||||
rowHeight={ROW_HEIGHT}
|
rowHeight={CELL_HEIGHT}
|
||||||
draggableHandle=".grid-drag-handle"
|
draggableHandle=".grid-drag-handle"
|
||||||
layout={layout}
|
layout={layout}
|
||||||
onLayoutChange={onLayoutChange}>
|
onLayoutChange={onLayoutChange}>
|
||||||
@ -37,21 +36,24 @@ function GridWrapper({size, layout, onLayoutChange, children}) {
|
|||||||
const SizedReactLayoutGrid = sizeMe({monitorWidth: true})(GridWrapper);
|
const SizedReactLayoutGrid = sizeMe({monitorWidth: true})(GridWrapper);
|
||||||
|
|
||||||
export interface DashboardGridProps {
|
export interface DashboardGridProps {
|
||||||
dashboard: DashboardModel;
|
getPanelContainer: () => PanelContainer;
|
||||||
getPanelLoader: () => PanelLoader;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DashboardGrid extends React.Component<DashboardGridProps, any> {
|
export class DashboardGrid extends React.Component<DashboardGridProps, any> {
|
||||||
gridToPanelMap: any;
|
gridToPanelMap: any;
|
||||||
|
panelContainer: PanelContainer;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
this.panelContainer = this.props.getPanelContainer();
|
||||||
this.onLayoutChange = this.onLayoutChange.bind(this);
|
this.onLayoutChange = this.onLayoutChange.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
buildLayout() {
|
buildLayout() {
|
||||||
const layout = [];
|
const layout = [];
|
||||||
for (let panel of this.props.dashboard.panels) {
|
const panels = this.panelContainer.getPanels();
|
||||||
|
|
||||||
|
for (let panel of panels) {
|
||||||
layout.push({
|
layout.push({
|
||||||
i: panel.id.toString(),
|
i: panel.id.toString(),
|
||||||
x: panel.x,
|
x: panel.x,
|
||||||
@ -60,21 +62,28 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
|
|||||||
h: panel.height,
|
h: panel.height,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.log(layout);
|
|
||||||
|
console.log('layout', layout);
|
||||||
return layout;
|
return layout;
|
||||||
}
|
}
|
||||||
|
|
||||||
onLayoutChange() {}
|
onLayoutChange() {}
|
||||||
|
|
||||||
renderPanels() {
|
renderPanels() {
|
||||||
|
const panels = this.panelContainer.getPanels();
|
||||||
const panelElements = [];
|
const panelElements = [];
|
||||||
for (let panel of this.props.dashboard.panels) {
|
|
||||||
|
for (let panel of panels) {
|
||||||
panelElements.push(
|
panelElements.push(
|
||||||
<div key={panel.id.toString()} className="panel">
|
<div key={panel.id.toString()} className="panel">
|
||||||
<DashboardPanel panel={panel} getPanelLoader={this.props.getPanelLoader} dashboard={this.props.dashboard} />
|
<DashboardPanel
|
||||||
|
panel={panel}
|
||||||
|
getPanelContainer={this.props.getPanelContainer}
|
||||||
|
/>
|
||||||
</div>,
|
</div>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return panelElements;
|
return panelElements;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,8 +97,5 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
coreModule.directive('dashboardGrid', function(reactDirective) {
|
coreModule.directive('dashboardGrid', function(reactDirective) {
|
||||||
return reactDirective(DashboardGrid, [
|
return reactDirective(DashboardGrid, [['getPanelContainer', {watchDepth: 'reference', wrapApply: false}]]);
|
||||||
['dashboard', {watchDepth: 'reference'}],
|
|
||||||
['getPanelLoader', {watchDepth: 'reference', wrapApply: false}],
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {PanelLoader} from './PanelLoader';
|
import {PanelModel} from '../PanelModel';
|
||||||
|
import {PanelContainer} from './PanelContainer';
|
||||||
|
import {AttachedPanel} from './PanelLoader';
|
||||||
|
|
||||||
export interface DashboardPanelProps {
|
export interface DashboardPanelProps {
|
||||||
panel: any;
|
panel: PanelModel;
|
||||||
dashboard: any;
|
getPanelContainer: () => PanelContainer;
|
||||||
getPanelLoader: () => PanelLoader;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DashboardPanel extends React.Component<DashboardPanelProps, any> {
|
export class DashboardPanel extends React.Component<DashboardPanelProps, any> {
|
||||||
private element: any;
|
element: any;
|
||||||
|
attachedPanel: AttachedPanel;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -16,8 +18,17 @@ export class DashboardPanel extends React.Component<DashboardPanelProps, any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
var loader = this.props.getPanelLoader();
|
const panelContainer = this.props.getPanelContainer();
|
||||||
loader.load(this.element, this.props.panel, this.props.dashboard);
|
const dashboard = panelContainer.getDashboard();
|
||||||
|
const loader = panelContainer.getPanelLoader();
|
||||||
|
|
||||||
|
this.attachedPanel = loader.load(this.element, this.props.panel, dashboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (this.attachedPanel) {
|
||||||
|
this.attachedPanel.destroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
11
public/app/features/dashboard/dashgrid/PanelContainer.ts
Normal file
11
public/app/features/dashboard/dashgrid/PanelContainer.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import {PanelModel} from '../PanelModel';
|
||||||
|
import {DashboardModel} from '../model';
|
||||||
|
import {PanelLoader} from './PanelLoader';
|
||||||
|
|
||||||
|
export interface PanelContainer {
|
||||||
|
getPanels(): PanelModel[];
|
||||||
|
getPanelLoader(): PanelLoader;
|
||||||
|
getDashboard(): DashboardModel;
|
||||||
|
panelPossitionUpdated(panel: PanelModel);
|
||||||
|
}
|
||||||
|
|
@ -23,6 +23,7 @@ export class PanelLoader {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
destroy: () => {
|
destroy: () => {
|
||||||
|
console.log('AttachedPanel:Destroy, id' + panel.id);
|
||||||
panelScope.$destroy();
|
panelScope.$destroy();
|
||||||
compiledElem.remove();
|
compiledElem.remove();
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
///<reference path="../../headers/common.d.ts" />
|
|
||||||
|
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
@ -8,18 +6,9 @@ import $ from 'jquery';
|
|||||||
import {DEFAULT_ANNOTATION_COLOR} from 'app/core/utils/colors';
|
import {DEFAULT_ANNOTATION_COLOR} from 'app/core/utils/colors';
|
||||||
import {Emitter, contextSrv, appEvents} from 'app/core/core';
|
import {Emitter, contextSrv, appEvents} from 'app/core/core';
|
||||||
import {DashboardRow} from './row/row_model';
|
import {DashboardRow} from './row/row_model';
|
||||||
|
import {PanelModel} from './PanelModel';
|
||||||
import sortByKeys from 'app/core/utils/sort_by_keys';
|
import sortByKeys from 'app/core/utils/sort_by_keys';
|
||||||
|
|
||||||
export interface Panel {
|
|
||||||
id: number;
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
type: string;
|
|
||||||
title: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CELL_HEIGHT = 30;
|
export const CELL_HEIGHT = 30;
|
||||||
export const CELL_VMARGIN = 15;
|
export const CELL_VMARGIN = 15;
|
||||||
|
|
||||||
@ -50,7 +39,7 @@ export class DashboardModel {
|
|||||||
events: any;
|
events: any;
|
||||||
editMode: boolean;
|
editMode: boolean;
|
||||||
folderId: number;
|
folderId: number;
|
||||||
panels: Panel[];
|
panels: PanelModel[];
|
||||||
|
|
||||||
constructor(data, meta?) {
|
constructor(data, meta?) {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
<div dash-class ng-if="dashboard">
|
<div dash-class ng-if="ctrl.dashboard">
|
||||||
<dashnav dashboard="dashboard"></dashnav>
|
<dashnav dashboard="ctrl.dashboard"></dashnav>
|
||||||
|
|
||||||
<div class="scroll-canvas scroll-canvas--dashboard">
|
<div class="scroll-canvas scroll-canvas--dashboard">
|
||||||
<div gemini-scrollbar>
|
<div gemini-scrollbar>
|
||||||
<div dash-editor-view class="dash-edit-view"></div>
|
<div dash-editor-view class="dash-edit-view"></div>
|
||||||
<div class="dashboard-container">
|
<div class="dashboard-container">
|
||||||
|
|
||||||
<dashboard-submenu ng-if="dashboard.meta.submenuEnabled" dashboard="dashboard"></dashboard-submenu>
|
<dashboard-submenu ng-if="ctrl.dashboard.meta.submenuEnabled" dashboard="ctrl.dashboard">
|
||||||
|
</dashboard-submenu>
|
||||||
|
|
||||||
<!-- <dash-grid dashboard="dashboard"></dash-grid> -->
|
<dashboard-grid get-panel-container="ctrl.getPanelContainer">
|
||||||
<dashboard-grid dashboard="dashboard" get-panel-loader="getPanelLoader">
|
|
||||||
</dashboard-grid>
|
</dashboard-grid>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user