legacy form migration
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { QueryEditorProps, SelectableValue } from '@grafana/data';
|
import { QueryEditorProps, SelectableValue } from '@grafana/data';
|
||||||
import { InlineField, InlineFieldRow, Select } from '@grafana/ui';
|
import { InlineField, Select } from '@grafana/ui';
|
||||||
import * as c from '../constants';
|
import * as c from '../constants';
|
||||||
import { migrate, DS_QUERY_SCHEMA } from '../migrations';
|
import { migrate, DS_QUERY_SCHEMA } from '../migrations';
|
||||||
import { ZabbixDatasource } from '../datasource';
|
import { ZabbixDatasource } from '../datasource';
|
||||||
@@ -14,6 +14,7 @@ import { ItemIdQueryEditor } from './QueryEditor/ItemIdQueryEditor';
|
|||||||
import { ServicesQueryEditor } from './QueryEditor/ServicesQueryEditor';
|
import { ServicesQueryEditor } from './QueryEditor/ServicesQueryEditor';
|
||||||
import { TriggersQueryEditor } from './QueryEditor/TriggersQueryEditor';
|
import { TriggersQueryEditor } from './QueryEditor/TriggersQueryEditor';
|
||||||
import { UserMacrosQueryEditor } from './QueryEditor/UserMacrosQueryEditor';
|
import { UserMacrosQueryEditor } from './QueryEditor/UserMacrosQueryEditor';
|
||||||
|
import { QueryEditorRow } from './QueryEditor/QueryEditorRow';
|
||||||
|
|
||||||
const zabbixQueryTypeOptions: Array<SelectableValue<string>> = [
|
const zabbixQueryTypeOptions: Array<SelectableValue<string>> = [
|
||||||
{
|
{
|
||||||
@@ -197,7 +198,7 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: ZabbixQ
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<InlineFieldRow>
|
<QueryEditorRow>
|
||||||
<InlineField label="Query type" labelWidth={12}>
|
<InlineField label="Query type" labelWidth={12}>
|
||||||
<Select
|
<Select
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
@@ -207,10 +208,7 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: ZabbixQ
|
|||||||
onChange={onPropChange('queryType')}
|
onChange={onPropChange('queryType')}
|
||||||
/>
|
/>
|
||||||
</InlineField>
|
</InlineField>
|
||||||
<div className="gf-form gf-form--grow">
|
</QueryEditorRow>
|
||||||
<div className="gf-form-label gf-form-label--grow" />
|
|
||||||
</div>
|
|
||||||
</InlineFieldRow>
|
|
||||||
{queryType === c.MODE_METRICS && renderMetricsEditor()}
|
{queryType === c.MODE_METRICS && renderMetricsEditor()}
|
||||||
{queryType === c.MODE_ITEMID && renderItemIdsEditor()}
|
{queryType === c.MODE_ITEMID && renderItemIdsEditor()}
|
||||||
{queryType === c.MODE_TEXT && renderTextMetricsEditor()}
|
{queryType === c.MODE_TEXT && renderTextMetricsEditor()}
|
||||||
|
|||||||
@@ -1,13 +1,24 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { InlineFieldRow } from '@grafana/ui';
|
import { InlineFieldRow, InlineFormLabel } from '@grafana/ui';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
export const QueryEditorRow = ({ children }: React.PropsWithChildren<{}>) => {
|
export const QueryEditorRow = ({ children }: React.PropsWithChildren<{}>) => {
|
||||||
|
const styles = getStyles();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InlineFieldRow>
|
<InlineFieldRow>
|
||||||
{children}
|
{children}
|
||||||
<div className="gf-form gf-form--grow">
|
<InlineFormLabel className={styles.rowTerminator}>
|
||||||
<div className="gf-form-label gf-form-label--grow" />
|
<></>
|
||||||
</div>
|
</InlineFormLabel>
|
||||||
</InlineFieldRow>
|
</InlineFieldRow>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getStyles = () => {
|
||||||
|
return {
|
||||||
|
rowTerminator: css({
|
||||||
|
flexGrow: 1,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { parseLegacyVariableQuery } from '../utils';
|
|||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
import { VariableQuery, VariableQueryData, VariableQueryProps, VariableQueryTypes } from '../types';
|
import { VariableQuery, VariableQueryData, VariableQueryProps, VariableQueryTypes } from '../types';
|
||||||
import { ZabbixInput } from './ZabbixInput';
|
import { ZabbixInput } from './ZabbixInput';
|
||||||
import { InlineFormLabel, Input, Select } from '@grafana/ui';
|
import { InlineField, InlineFieldRow, InlineFormLabel, Input, Select } from '@grafana/ui';
|
||||||
|
|
||||||
export class ZabbixVariableQueryEditor extends PureComponent<VariableQueryProps, VariableQueryData> {
|
export class ZabbixVariableQueryEditor extends PureComponent<VariableQueryProps, VariableQueryData> {
|
||||||
queryTypes: Array<SelectableValue<VariableQueryTypes>> = [
|
queryTypes: Array<SelectableValue<VariableQueryTypes>> = [
|
||||||
@@ -94,81 +94,95 @@ export class ZabbixVariableQueryEditor extends PureComponent<VariableQueryProps,
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="gf-form max-width-21">
|
<InlineFieldRow>
|
||||||
<InlineFormLabel width={10}>Query Type</InlineFormLabel>
|
<InlineField label="Query Type" labelWidth={16}>
|
||||||
<Select
|
<Select
|
||||||
width={11}
|
width={30}
|
||||||
value={selectedQueryType}
|
value={selectedQueryType}
|
||||||
options={this.queryTypes}
|
options={this.queryTypes}
|
||||||
onChange={this.handleQueryTypeChange}
|
onChange={this.handleQueryTypeChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</InlineField>
|
||||||
<div className="gf-form-inline">
|
</InlineFieldRow>
|
||||||
<div className="gf-form max-width-30">
|
|
||||||
<InlineFormLabel width={10}>Group</InlineFormLabel>
|
<InlineFieldRow>
|
||||||
|
<InlineField label="Group" labelWidth={16}>
|
||||||
<ZabbixInput
|
<ZabbixInput
|
||||||
|
width={30}
|
||||||
value={group}
|
value={group}
|
||||||
onChange={(evt) => this.handleQueryUpdate(evt, 'group')}
|
onChange={(evt) => this.handleQueryUpdate(evt, 'group')}
|
||||||
onBlur={this.handleQueryChange}
|
onBlur={this.handleQueryChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</InlineField>
|
||||||
{selectedQueryType.value !== VariableQueryTypes.Group && (
|
</InlineFieldRow>
|
||||||
<div className="gf-form max-width-30">
|
|
||||||
<InlineFormLabel width={10}>Host</InlineFormLabel>
|
{selectedQueryType.value !== VariableQueryTypes.Group && (
|
||||||
|
<InlineFieldRow>
|
||||||
|
<InlineField label="Host" labelWidth={16}>
|
||||||
<ZabbixInput
|
<ZabbixInput
|
||||||
|
width={30}
|
||||||
value={host}
|
value={host}
|
||||||
onChange={(evt) => this.handleQueryUpdate(evt, 'host')}
|
onChange={(evt) => this.handleQueryUpdate(evt, 'host')}
|
||||||
onBlur={this.handleQueryChange}
|
onBlur={this.handleQueryChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</InlineField>
|
||||||
)}
|
</InlineFieldRow>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{(selectedQueryType.value === VariableQueryTypes.Application ||
|
{(selectedQueryType.value === VariableQueryTypes.Application ||
|
||||||
selectedQueryType.value === VariableQueryTypes.ItemTag ||
|
selectedQueryType.value === VariableQueryTypes.ItemTag ||
|
||||||
selectedQueryType.value === VariableQueryTypes.Item ||
|
selectedQueryType.value === VariableQueryTypes.Item ||
|
||||||
selectedQueryType.value === VariableQueryTypes.ItemValues) && (
|
selectedQueryType.value === VariableQueryTypes.ItemValues) && (
|
||||||
<div className="gf-form-inline">
|
<>
|
||||||
{supportsItemTags && (
|
{supportsItemTags && (
|
||||||
<div className="gf-form max-width-30">
|
<InlineFieldRow>
|
||||||
<InlineFormLabel width={10}>Item tag</InlineFormLabel>
|
<InlineField label="Item Tag" labelWidth={16}>
|
||||||
<ZabbixInput
|
<ZabbixInput
|
||||||
value={itemTag}
|
width={30}
|
||||||
onChange={(evt) => this.handleQueryUpdate(evt, 'itemTag')}
|
value={itemTag}
|
||||||
onBlur={this.handleQueryChange}
|
onChange={(evt) => this.handleQueryUpdate(evt, 'itemTag')}
|
||||||
/>
|
onBlur={this.handleQueryChange}
|
||||||
</div>
|
/>
|
||||||
|
</InlineField>
|
||||||
|
</InlineFieldRow>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!supportsItemTags && (
|
{!supportsItemTags && (
|
||||||
<div className="gf-form max-width-30">
|
<InlineFieldRow>
|
||||||
<InlineFormLabel width={10}>Application</InlineFormLabel>
|
<InlineField label="Application" labelWidth={16}>
|
||||||
<ZabbixInput
|
<ZabbixInput
|
||||||
value={application}
|
width={30}
|
||||||
onChange={(evt) => this.handleQueryUpdate(evt, 'application')}
|
value={application}
|
||||||
onBlur={this.handleQueryChange}
|
onChange={(evt) => this.handleQueryUpdate(evt, 'application')}
|
||||||
/>
|
onBlur={this.handleQueryChange}
|
||||||
</div>
|
/>
|
||||||
|
</InlineField>
|
||||||
|
</InlineFieldRow>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(selectedQueryType.value === VariableQueryTypes.Item ||
|
{(selectedQueryType.value === VariableQueryTypes.Item ||
|
||||||
selectedQueryType.value === VariableQueryTypes.ItemValues) && (
|
selectedQueryType.value === VariableQueryTypes.ItemValues) && (
|
||||||
<div className="gf-form max-width-30">
|
<InlineFieldRow>
|
||||||
<InlineFormLabel width={10}>Item</InlineFormLabel>
|
<InlineField label="Item" labelWidth={16}>
|
||||||
<ZabbixInput
|
<ZabbixInput
|
||||||
value={item}
|
width={30}
|
||||||
onChange={(evt) => this.handleQueryUpdate(evt, 'item')}
|
value={item}
|
||||||
onBlur={this.handleQueryChange}
|
onChange={(evt) => this.handleQueryUpdate(evt, 'item')}
|
||||||
/>
|
onBlur={this.handleQueryChange}
|
||||||
</div>
|
/>
|
||||||
|
</InlineField>
|
||||||
|
</InlineFieldRow>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{legacyQuery && (
|
{legacyQuery && (
|
||||||
<div className="gf-form">
|
<>
|
||||||
<InlineFormLabel width={10} tooltip="Original query string, read-only">
|
<InlineFormLabel width={10} tooltip="Original query string, read-only">
|
||||||
Legacy Query
|
Legacy Query
|
||||||
</InlineFormLabel>
|
</InlineFormLabel>
|
||||||
<Input value={legacyQuery} readOnly={true} />
|
<Input value={legacyQuery} readOnly={true} />
|
||||||
</div>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data';
|
|||||||
import { isRegex, variableRegex } from '../utils';
|
import { isRegex, variableRegex } from '../utils';
|
||||||
|
|
||||||
import * as grafanaUi from '@grafana/ui';
|
import * as grafanaUi from '@grafana/ui';
|
||||||
const Input = (grafanaUi as any).LegacyForms?.Input || (grafanaUi as any).Input;
|
const Input = (grafanaUi as any).Input || (grafanaUi as any).LegacyForms?.Input;
|
||||||
|
|
||||||
const variablePattern = RegExp(`^${variableRegex.source}`);
|
const variablePattern = RegExp(`^${variableRegex.source}`);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { cx, css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import {
|
import {
|
||||||
ZBX_ACK_ACTION_ADD_MESSAGE,
|
ZBX_ACK_ACTION_ADD_MESSAGE,
|
||||||
ZBX_ACK_ACTION_ACK,
|
ZBX_ACK_ACTION_ACK,
|
||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
withTheme,
|
withTheme,
|
||||||
Themeable,
|
Themeable,
|
||||||
TextArea,
|
TextArea,
|
||||||
|
ButtonGroup,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
import { FAIcon } from '../../components';
|
import { FAIcon } from '../../components';
|
||||||
import { GrafanaTheme } from '@grafana/data';
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
@@ -200,63 +201,49 @@ export class AckModalUnthemed extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { theme } = this.props;
|
const { theme } = this.props;
|
||||||
|
|
||||||
const styles = getStyles(theme);
|
const styles = getStyles(theme);
|
||||||
const modalClass = cx(styles.modal);
|
|
||||||
const modalTitleClass = cx(styles.modalHeaderTitle);
|
|
||||||
const inputGroupClass = cx('gf-form', styles.inputGroup);
|
|
||||||
const inputClass = cx(this.state.error && styles.input);
|
|
||||||
const inputHintClass = cx('gf-form-hint-text', styles.inputHint);
|
|
||||||
const inputErrorClass = cx('gf-form-hint-text', styles.inputError);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
onDismiss={this.dismiss}
|
onDismiss={this.dismiss}
|
||||||
className={modalClass}
|
className={styles.modal}
|
||||||
title={
|
title={
|
||||||
<div className={modalTitleClass}>
|
<div className={styles.modalHeaderTitle}>
|
||||||
{this.state.loading ? <Spinner size={18} /> : <FAIcon icon="reply-all" />}
|
{this.state.loading ? <Spinner size={18} /> : <FAIcon icon="reply-all" />}
|
||||||
<span className="p-l-1">Acknowledge Problem</span>
|
Acknowledge Problem
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className={inputGroupClass}>
|
<div className={styles.inputGroup}>
|
||||||
<label className="gf-form-hint">
|
<TextArea
|
||||||
<TextArea
|
className={this.state.error && styles.input}
|
||||||
className={inputClass}
|
type="text"
|
||||||
type="text"
|
name="message"
|
||||||
name="message"
|
placeholder="Message"
|
||||||
placeholder="Message"
|
autoComplete="off"
|
||||||
autoComplete="off"
|
autoFocus={true}
|
||||||
autoFocus={true}
|
value={this.state.value}
|
||||||
value={this.state.value}
|
onChange={this.handleChange}
|
||||||
onChange={this.handleChange}
|
onKeyDown={this.handleKeyPress}
|
||||||
onKeyDown={this.handleKeyPress}
|
/>
|
||||||
></TextArea>
|
<small className={styles.inputHint}>Press Enter to submit</small>
|
||||||
<small className={inputHintClass}>Press Enter to submit</small>
|
{this.state.error && <small className={styles.inputError}>{this.state.errorMessage}</small>}
|
||||||
{this.state.error && <small className={inputErrorClass}>{this.state.errorMessage}</small>}
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="gf-form">
|
<VerticalGroup>{this.renderActions()}</VerticalGroup>
|
||||||
<VerticalGroup>{this.renderActions()}</VerticalGroup>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{this.state.ackError && (
|
{this.state.ackError && <span className={styles.ackError}>{this.state.ackError}</span>}
|
||||||
<div className="gf-form ack-request-error">
|
|
||||||
<span className={styles.ackError}>{this.state.ackError}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="gf-form-button-row text-center">
|
<ButtonGroup className={styles.buttonGroup}>
|
||||||
<Button variant="primary" onClick={this.submit}>
|
<Button variant="primary" onClick={this.submit}>
|
||||||
Update
|
Update
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button variant="secondary" onClick={this.dismiss}>
|
<Button variant="secondary" onClick={this.dismiss}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</ButtonGroup>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -297,6 +284,11 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
|||||||
ackError: css`
|
ackError: css`
|
||||||
color: ${red};
|
color: ${red};
|
||||||
`,
|
`,
|
||||||
|
buttonGroup: css`
|
||||||
|
justify-content: center;
|
||||||
|
gap: ${theme.spacing.sm};
|
||||||
|
margin-top: ${theme.spacing.md};
|
||||||
|
`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { cx, css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { GrafanaTheme, SelectableValue } from '@grafana/data';
|
import { GrafanaTheme, SelectableValue } from '@grafana/data';
|
||||||
import { Button, Spinner, Modal, Select, stylesFactory, withTheme, Themeable } from '@grafana/ui';
|
import { Button, Spinner, Modal, Select, stylesFactory, withTheme, Themeable, ButtonGroup } from '@grafana/ui';
|
||||||
import { ZBXScript, APIExecuteScriptResponse } from '../../datasource/zabbix/connectors/zabbix_api/types';
|
import { ZBXScript, APIExecuteScriptResponse } from '../../datasource/zabbix/connectors/zabbix_api/types';
|
||||||
import { FAIcon } from '../../components';
|
import { FAIcon } from '../../components';
|
||||||
|
|
||||||
@@ -118,32 +118,24 @@ export class ExecScriptModalUnthemed extends PureComponent<Props, State> {
|
|||||||
const { scriptOptions, selectedScript, script, result, selectError, errorMessage, error } = this.state;
|
const { scriptOptions, selectedScript, script, result, selectError, errorMessage, error } = this.state;
|
||||||
|
|
||||||
const styles = getStyles(theme);
|
const styles = getStyles(theme);
|
||||||
const modalClass = cx(styles.modal);
|
|
||||||
const modalTitleClass = cx(styles.modalHeaderTitle);
|
|
||||||
const selectErrorClass = cx('gf-form-hint-text', styles.inputError);
|
|
||||||
const scriptCommandContainerClass = cx('gf-form', styles.scriptCommandContainer);
|
|
||||||
const scriptCommandClass = cx('gf-form-hint-text', styles.scriptCommand);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
onDismiss={this.dismiss}
|
onDismiss={this.dismiss}
|
||||||
className={modalClass}
|
className={styles.modal}
|
||||||
title={
|
title={
|
||||||
<div className={modalTitleClass}>
|
<div className={styles.modalHeaderTitle}>
|
||||||
{this.state.loading ? <Spinner size={18} /> : <FAIcon icon="terminal" />}
|
{this.state.loading ? <Spinner size={18} /> : <FAIcon icon="terminal" />}
|
||||||
<span className="p-l-1">Execute script</span>
|
Execute script
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="gf-form">
|
<Select options={scriptOptions} value={selectedScript} onChange={this.onChangeSelectedScript} />
|
||||||
<label className="gf-form-hint">
|
{selectError && <small className={styles.inputError}>{selectError}</small>}
|
||||||
<Select options={scriptOptions} value={selectedScript} onChange={this.onChangeSelectedScript} />
|
|
||||||
{selectError && <small className={selectErrorClass}>{selectError}</small>}
|
<div className={styles.scriptCommandContainer}>
|
||||||
</label>
|
{script && <small className={styles.scriptCommand}>{script.command}</small>}
|
||||||
</div>
|
|
||||||
<div className={scriptCommandContainerClass}>
|
|
||||||
{script && <small className={scriptCommandClass}>{script.command}</small>}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.resultContainer}>
|
<div className={styles.resultContainer}>
|
||||||
@@ -151,14 +143,15 @@ export class ExecScriptModalUnthemed extends PureComponent<Props, State> {
|
|||||||
{error && <span className={styles.execError}>{errorMessage}</span>}
|
{error && <span className={styles.execError}>{errorMessage}</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="gf-form-button-row text-center">
|
<ButtonGroup className={styles.buttonGroup}>
|
||||||
<Button variant="primary" onClick={this.submit}>
|
<Button variant="primary" onClick={this.submit}>
|
||||||
Execute
|
Execute
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button variant="secondary" onClick={this.dismiss}>
|
<Button variant="secondary" onClick={this.dismiss}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</ButtonGroup>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -214,6 +207,11 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
|||||||
execError: css`
|
execError: css`
|
||||||
color: ${red};
|
color: ${red};
|
||||||
`,
|
`,
|
||||||
|
buttonGroup: css`
|
||||||
|
justify-content: center;
|
||||||
|
gap: ${theme.spacing.sm};
|
||||||
|
margin-top: ${theme.spacing.md};
|
||||||
|
`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user