mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-04-15 06:08:37 +01:00
Complete new cause entity list
This commit is contained in:
parent
4f632b234f
commit
13f94e83f0
@ -295,7 +295,25 @@ input::-webkit-inner-spin-button {
|
||||
|
||||
.and-or-svg {
|
||||
margin-top: -8px;
|
||||
margin-right: -5px;
|
||||
margin-right: -26px;
|
||||
}
|
||||
|
||||
.and-or-label {
|
||||
margin-left: 52px;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
margin-left: 69px;
|
||||
}
|
||||
.data-type-label {
|
||||
margin-left: 54px;
|
||||
}
|
||||
|
||||
.operator-label {
|
||||
margin-left: 80px;
|
||||
}
|
||||
.value-label{
|
||||
margin-left: 62px;
|
||||
}
|
||||
|
||||
.scroll-box {
|
||||
|
150
src/Explorer/Tabs/QueryTablesTab/QueryTableEntityClause.tsx
Normal file
150
src/Explorer/Tabs/QueryTablesTab/QueryTableEntityClause.tsx
Normal file
@ -0,0 +1,150 @@
|
||||
import {
|
||||
Checkbox,
|
||||
Dropdown,
|
||||
IDropdownOption,
|
||||
IDropdownStyles,
|
||||
IImageProps,
|
||||
Image,
|
||||
IStackTokens,
|
||||
Stack,
|
||||
TextField,
|
||||
TooltipHost
|
||||
} from "@fluentui/react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import AddIcon from "../../../../images/Add.svg";
|
||||
import CancelIcon from "../../../../images/cancel.svg";
|
||||
import { IOption } from "./QueryTableTabUtils";
|
||||
const dropdownStyles: Partial<IDropdownStyles> = { dropdown: { width: 100 } };
|
||||
|
||||
export interface IQueryTableEntityClauseProps {
|
||||
entityValue: string;
|
||||
entityValuePlaceHolder?: string;
|
||||
selectedOperator: string;
|
||||
selectedOperation: string;
|
||||
opertorOptions: IOption[];
|
||||
opertationOptions: IOption[];
|
||||
isQueryTableEntityChecked: boolean;
|
||||
selectedField: string;
|
||||
fieldOptions: IOption[];
|
||||
entityTypeOptions: IOption[];
|
||||
selectedEntityType: string;
|
||||
isTimeStampSelected?: boolean;
|
||||
selectedTimestamp: string;
|
||||
timestampOptions: IOption[];
|
||||
onAddNewClause?: () => void;
|
||||
onDeleteClause?: () => void;
|
||||
onQueryTableEntityCheck: (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean) => void;
|
||||
onDropdownChange: (selectedOption: IDropdownOption, selectedOptionType: string) => void;
|
||||
onEntityValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
|
||||
}
|
||||
|
||||
export const QueryTableEntityClause: FunctionComponent<IQueryTableEntityClauseProps> = ({
|
||||
entityValue,
|
||||
entityValuePlaceHolder,
|
||||
selectedOperator,
|
||||
opertorOptions,
|
||||
selectedField,
|
||||
isQueryTableEntityChecked,
|
||||
fieldOptions,
|
||||
entityTypeOptions,
|
||||
selectedEntityType,
|
||||
selectedOperation,
|
||||
opertationOptions,
|
||||
isTimeStampSelected,
|
||||
selectedTimestamp,
|
||||
timestampOptions,
|
||||
onQueryTableEntityCheck,
|
||||
onAddNewClause,
|
||||
onDeleteClause,
|
||||
onDropdownChange,
|
||||
onEntityValueChange,
|
||||
}: IQueryTableEntityClauseProps): JSX.Element => {
|
||||
const cancelImageProps: IImageProps = {
|
||||
width: 14,
|
||||
height: 25,
|
||||
};
|
||||
|
||||
const addImageProps: IImageProps = {
|
||||
width: 18,
|
||||
height: 25,
|
||||
};
|
||||
|
||||
const sectionStackTokens: IStackTokens = { childrenGap: 12 };
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack horizontal tokens={sectionStackTokens}>
|
||||
<TooltipHost content="Add new clause" id="addNewClause">
|
||||
<Image {...addImageProps} src={AddIcon} alt="Add new clause" id="addNewClause" onClick={onAddNewClause} />
|
||||
</TooltipHost>
|
||||
<TooltipHost content="Delete clause" id="deleteClause">
|
||||
<Image
|
||||
{...cancelImageProps}
|
||||
src={CancelIcon}
|
||||
alt="delete clause"
|
||||
id="deleteClause"
|
||||
onClick={onDeleteClause}
|
||||
/>
|
||||
</TooltipHost>
|
||||
<Checkbox checked={isQueryTableEntityChecked} onChange={onQueryTableEntityCheck} />
|
||||
<Dropdown
|
||||
selectedKey={selectedOperation}
|
||||
onChange={(_event: React.FormEvent<HTMLElement>, selectedOption: IDropdownOption) =>
|
||||
onDropdownChange(selectedOption, "selectedOperation")
|
||||
}
|
||||
options={opertationOptions}
|
||||
id="operatorOptionId"
|
||||
styles={dropdownStyles}
|
||||
/>
|
||||
<Dropdown
|
||||
selectedKey={selectedField}
|
||||
onChange={(_event: React.FormEvent<HTMLElement>, selectedOption: IDropdownOption) =>
|
||||
onDropdownChange(selectedOption, "selectedField")
|
||||
}
|
||||
options={fieldOptions}
|
||||
id="fieldOptionId"
|
||||
styles={dropdownStyles}
|
||||
/>
|
||||
<Dropdown
|
||||
selectedKey={selectedEntityType}
|
||||
onChange={(_event: React.FormEvent<HTMLElement>, selectedOption: IDropdownOption) =>
|
||||
onDropdownChange(selectedOption, "selectedEntityType")
|
||||
}
|
||||
options={entityTypeOptions}
|
||||
id="entityOptionId"
|
||||
disabled={selectedField !== "t3PN"}
|
||||
styles={dropdownStyles}
|
||||
/>
|
||||
<Dropdown
|
||||
selectedKey={selectedOperator}
|
||||
onChange={(_event: React.FormEvent<HTMLElement>, selectedOption: IDropdownOption) =>
|
||||
onDropdownChange(selectedOption, "selectedOperator")
|
||||
}
|
||||
options={opertorOptions}
|
||||
id="operatorOptionId"
|
||||
styles={dropdownStyles}
|
||||
/>
|
||||
{isTimeStampSelected ? (
|
||||
<Dropdown
|
||||
selectedKey={selectedTimestamp}
|
||||
onChange={(_event: React.FormEvent<HTMLElement>, selectedOption: IDropdownOption) =>
|
||||
onDropdownChange(selectedOption, "selectedTimestamp")
|
||||
}
|
||||
options={timestampOptions}
|
||||
id="operatorOptionId"
|
||||
styles={dropdownStyles}
|
||||
/>
|
||||
) : (
|
||||
<TextField
|
||||
id="entityValueId"
|
||||
autoFocus
|
||||
placeholder={entityValuePlaceHolder}
|
||||
value={entityValue}
|
||||
onChange={onEntityValueChange}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
110
src/Explorer/Tabs/QueryTablesTab/QueryTableTabUtils.tsx
Normal file
110
src/Explorer/Tabs/QueryTablesTab/QueryTableTabUtils.tsx
Normal file
@ -0,0 +1,110 @@
|
||||
import { IColumn } from "@fluentui/react";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import Explorer from "../../Explorer";
|
||||
import TableEntityListViewModel from "../../Tables/DataTable/TableEntityListViewModel";
|
||||
import QueryViewModel from "../../Tables/QueryBuilder/QueryViewModel";
|
||||
import TabsBase from "../TabsBase";
|
||||
import NewQueryTablesTab from "./QueryTablesTab";
|
||||
|
||||
export interface Button {
|
||||
visible: boolean;
|
||||
enabled: boolean;
|
||||
isSelected?: boolean;
|
||||
}
|
||||
|
||||
export interface IOption {
|
||||
key: string;
|
||||
text: string;
|
||||
}
|
||||
export interface IDocument {
|
||||
partitionKey: string;
|
||||
rowKey: string;
|
||||
timeStamp: string;
|
||||
}
|
||||
export interface IQueryTablesTabComponentProps {
|
||||
tabKind: ViewModels.CollectionTabKind;
|
||||
title: string;
|
||||
tabPath: string;
|
||||
collection: ViewModels.CollectionBase;
|
||||
node: ViewModels.TreeNode;
|
||||
onLoadStartKey: number;
|
||||
container: Explorer;
|
||||
tabsBaseInstance: TabsBase;
|
||||
queryTablesTab: NewQueryTablesTab;
|
||||
}
|
||||
|
||||
export interface IQueryTablesTabComponentStates {
|
||||
tableEntityListViewModel: TableEntityListViewModel;
|
||||
queryViewModel: QueryViewModel;
|
||||
queryText: string;
|
||||
selectedQueryText: string;
|
||||
executeQueryButton: Button;
|
||||
queryBuilderButton: Button;
|
||||
queryTextButton: Button;
|
||||
addEntityButton: Button;
|
||||
editEntityButton: Button;
|
||||
deleteEntityButton: Button;
|
||||
isHelperActive: boolean;
|
||||
columns: IColumn[];
|
||||
items: IDocument[];
|
||||
isExpanded: boolean;
|
||||
queryTableRows: IQueryTableRowsType[];
|
||||
}
|
||||
|
||||
export interface IQueryTableRowsType {
|
||||
isQueryTableEntityChecked: boolean;
|
||||
isTimeStampSelected: boolean;
|
||||
selectedOperator: string;
|
||||
selectedField: string;
|
||||
entityValue: string;
|
||||
selectedEntityType: string;
|
||||
selectedOperation: string;
|
||||
selectedTimestamp: string;
|
||||
fieldOptions: IOption[];
|
||||
opertorOptions: IOption[];
|
||||
entityTypeOptions: IOption[];
|
||||
opertionOptions: IOption[];
|
||||
timestampOptions: IOption[];
|
||||
id: number;
|
||||
}
|
||||
|
||||
export const opertionOptions = [
|
||||
{ key: "And", text: "And" },
|
||||
{ key: "Or", text: "Or" },
|
||||
];
|
||||
export const opertorOptions = [
|
||||
{ key: "=", text: "=" },
|
||||
{ key: ">", text: ">" },
|
||||
{ key: ">=", text: ">=" },
|
||||
{ key: "<", text: "<" },
|
||||
{ key: "<=", text: "<=" },
|
||||
{ key: "<>", text: "<>" },
|
||||
];
|
||||
|
||||
export const fieldOptions = [
|
||||
{ key: "PartitionKey", text: "PartitionKey" },
|
||||
{ key: "RowKey", text: "RowKey" },
|
||||
{ key: "Timestamp", text: "Timestamp" },
|
||||
{ key: "t3PN", text: "t3PN" },
|
||||
];
|
||||
|
||||
export const entityTypeOptions = [
|
||||
{ key: "String", text: "String" },
|
||||
{ key: "Boolean", text: "Boolean" },
|
||||
{ key: "Binary", text: "Binary" },
|
||||
{ key: "DateTime", text: "DateTime" },
|
||||
{ key: "Double", text: "Double" },
|
||||
{ key: "Guid", text: "Guid" },
|
||||
{ key: "Int32", text: "Int32" },
|
||||
{ key: "Int64", text: "Int64" },
|
||||
];
|
||||
|
||||
export const timestampOptions = [
|
||||
{ key: "Last hour", text: "Last hour" },
|
||||
{ key: "Last 24 hours", text: "Last 24 hours" },
|
||||
{ key: "Last 7 days", text: "Last 7 days" },
|
||||
{ key: "Last 31 days", text: "Last 31 days" },
|
||||
{ key: "Last 365 days", text: "Last 365 days" },
|
||||
{ key: "Current month", text: "Current month" },
|
||||
{ key: "Current year", text: "Current year" },
|
||||
];
|
@ -1,4 +1,4 @@
|
||||
import { DetailsList, DetailsListLayoutMode, IColumn, SelectionMode } from "@fluentui/react";
|
||||
import { DetailsList, DetailsListLayoutMode, IColumn, IDropdownOption, SelectionMode } from "@fluentui/react";
|
||||
import * as ko from "knockout";
|
||||
import React, { Component } from "react";
|
||||
import QueryInformation from "../../../../images//QueryBuilder/QueryInformation_16x.png";
|
||||
@ -26,47 +26,18 @@ import TableCommands from "../../Tables/DataTable/TableCommands";
|
||||
import TableEntityListViewModel from "../../Tables/DataTable/TableEntityListViewModel";
|
||||
import QueryViewModel from "../../Tables/QueryBuilder/QueryViewModel";
|
||||
import { CassandraAPIDataClient, TableDataClient } from "../../Tables/TableDataClient";
|
||||
import TabsBase from "../TabsBase";
|
||||
import NewQueryTablesTab from "./QueryTablesTab";
|
||||
|
||||
export interface Button {
|
||||
visible: boolean;
|
||||
enabled: boolean;
|
||||
isSelected?: boolean;
|
||||
}
|
||||
export interface IDocument {
|
||||
partitionKey: string;
|
||||
rowKey: string;
|
||||
timeStamp: string;
|
||||
}
|
||||
export interface IQueryTablesTabComponentProps {
|
||||
tabKind: ViewModels.CollectionTabKind;
|
||||
title: string;
|
||||
tabPath: string;
|
||||
collection: ViewModels.CollectionBase;
|
||||
node: ViewModels.TreeNode;
|
||||
onLoadStartKey: number;
|
||||
container: Explorer;
|
||||
tabsBaseInstance: TabsBase;
|
||||
queryTablesTab: NewQueryTablesTab;
|
||||
}
|
||||
|
||||
interface IQueryTablesTabComponentStates {
|
||||
tableEntityListViewModel: TableEntityListViewModel;
|
||||
queryViewModel: QueryViewModel;
|
||||
queryText: string;
|
||||
selectedQueryText: string;
|
||||
executeQueryButton: Button;
|
||||
queryBuilderButton: Button;
|
||||
queryTextButton: Button;
|
||||
addEntityButton: Button;
|
||||
editEntityButton: Button;
|
||||
deleteEntityButton: Button;
|
||||
isHelperActive: boolean;
|
||||
columns: IColumn[];
|
||||
items: IDocument[];
|
||||
isExpanded: boolean;
|
||||
}
|
||||
import { QueryTableEntityClause } from "./QueryTableEntityClause";
|
||||
import {
|
||||
entityTypeOptions,
|
||||
fieldOptions,
|
||||
IDocument,
|
||||
IQueryTableRowsType,
|
||||
IQueryTablesTabComponentProps,
|
||||
IQueryTablesTabComponentStates,
|
||||
opertionOptions,
|
||||
opertorOptions,
|
||||
timestampOptions,
|
||||
} from "./QueryTableTabUtils";
|
||||
|
||||
class QueryTablesTabComponent extends Component<IQueryTablesTabComponentProps, IQueryTablesTabComponentStates> {
|
||||
// public readonly html = template;
|
||||
@ -186,6 +157,24 @@ class QueryTablesTabComponent extends Component<IQueryTablesTabComponentProps, I
|
||||
columns: columns,
|
||||
items: [],
|
||||
isExpanded: false,
|
||||
queryTableRows: [
|
||||
{
|
||||
isQueryTableEntityChecked: false,
|
||||
selectedOperator: "=",
|
||||
opertorOptions,
|
||||
selectedField: "PartitionKey",
|
||||
fieldOptions,
|
||||
id: 1,
|
||||
entityTypeOptions,
|
||||
selectedEntityType: "String",
|
||||
opertionOptions,
|
||||
selectedOperation: "And",
|
||||
entityValue: "",
|
||||
isTimeStampSelected: false,
|
||||
timestampOptions,
|
||||
selectedTimestamp: "Last hour",
|
||||
},
|
||||
],
|
||||
};
|
||||
this.state.tableEntityListViewModel.queryTablesTab = this.props.queryTablesTab;
|
||||
// console.log(
|
||||
@ -427,8 +416,68 @@ class QueryTablesTabComponent extends Component<IQueryTablesTabComponentProps, I
|
||||
this.state.queryViewModel.toggleAdvancedOptions();
|
||||
}
|
||||
|
||||
private onQueryTableEntityCheck = (index: number): void => {
|
||||
const { queryTableRows } = this.state;
|
||||
const cloneQueryTableRows: IQueryTableRowsType[] = [...queryTableRows];
|
||||
cloneQueryTableRows[index].isQueryTableEntityChecked = !cloneQueryTableRows[index].isQueryTableEntityChecked;
|
||||
this.setState({ queryTableRows: cloneQueryTableRows });
|
||||
};
|
||||
|
||||
private onDropdownChange = (selectedOption: IDropdownOption, selectedOptionType: string, index: number): void => {
|
||||
const { queryTableRows } = this.state;
|
||||
const cloneQueryTableRows: IQueryTableRowsType[] = [...queryTableRows];
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
cloneQueryTableRows[index][selectedOptionType] = selectedOption.text;
|
||||
const { text } = selectedOption;
|
||||
if (text === "DateTime" || text === "Timestamp") {
|
||||
cloneQueryTableRows[index].isTimeStampSelected = true;
|
||||
cloneQueryTableRows[index].selectedEntityType = "DateTime";
|
||||
} else if (selectedOptionType !== "selectedTimestamp") {
|
||||
cloneQueryTableRows[index].isTimeStampSelected = false;
|
||||
}
|
||||
this.setState({ queryTableRows: cloneQueryTableRows });
|
||||
};
|
||||
|
||||
onDeleteClause = (indexToRemove: number): void => {
|
||||
const { queryTableRows } = this.state;
|
||||
const cloneQueryTableRows: IQueryTableRowsType[] = [...queryTableRows];
|
||||
cloneQueryTableRows.splice(indexToRemove, 1);
|
||||
this.setState({ queryTableRows: cloneQueryTableRows });
|
||||
};
|
||||
|
||||
onAddNewClause = (): void => {
|
||||
const { queryTableRows } = this.state;
|
||||
const cloneQueryTableRows: IQueryTableRowsType[] = [...queryTableRows];
|
||||
cloneQueryTableRows.splice(cloneQueryTableRows.length, 0, {
|
||||
isQueryTableEntityChecked: false,
|
||||
selectedOperator: "=",
|
||||
opertorOptions,
|
||||
id: cloneQueryTableRows.length + 1,
|
||||
selectedField: "PartitionKey",
|
||||
fieldOptions,
|
||||
entityTypeOptions,
|
||||
selectedEntityType: "String",
|
||||
opertionOptions,
|
||||
selectedOperation: "And",
|
||||
entityValue: "",
|
||||
isTimeStampSelected: false,
|
||||
timestampOptions,
|
||||
selectedTimestamp: "Last hour",
|
||||
});
|
||||
this.setState({ queryTableRows: cloneQueryTableRows });
|
||||
};
|
||||
|
||||
private onEntityValueChange = (newInput: string, index: number) => {
|
||||
const { queryTableRows } = this.state;
|
||||
const cloneQueryTableRows: IQueryTableRowsType[] = [...queryTableRows];
|
||||
cloneQueryTableRows[index].entityValue = newInput;
|
||||
this.setState({ queryTableRows: cloneQueryTableRows });
|
||||
};
|
||||
|
||||
render(): JSX.Element {
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
const { queryTableRows } = this.state;
|
||||
|
||||
return (
|
||||
<div className="tab-pane tableContainer" id={this.props.tabsBaseInstance.tabId} role="tabpanel">
|
||||
@ -476,30 +525,57 @@ class QueryTablesTabComponent extends Component<IQueryTablesTabComponentProps, I
|
||||
</th>
|
||||
<th className="clause-table-cell header-background"></th>
|
||||
<th className="clause-table-cell header-background and-or-header">
|
||||
<span>{this.andLabel}</span>
|
||||
<span className="and-or-label">{this.andLabel}</span>
|
||||
</th>
|
||||
<th className="clause-table-cell header-background field-header">
|
||||
<span>{this.fieldLabel}</span>
|
||||
<span className="field-label">{this.fieldLabel}</span>
|
||||
</th>
|
||||
<th className="clause-table-cell header-background type-header">
|
||||
<span>{this.dataTypeLabel}</span>
|
||||
<span className="data-type-label">{this.dataTypeLabel}</span>
|
||||
</th>
|
||||
<th className="clause-table-cell header-background operator-header">
|
||||
<span>{this.operatorLabel}</span>
|
||||
<span className="operator-label">{this.operatorLabel}</span>
|
||||
</th>
|
||||
<th className="clause-table-cell header-background value-header">
|
||||
<span>{this.valueLabel}</span>
|
||||
<span className="value-label">{this.valueLabel}</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<>
|
||||
{queryTableRows.map((queryTableRow, index) => (
|
||||
<QueryTableEntityClause
|
||||
key={queryTableRow.id}
|
||||
isQueryTableEntityChecked={queryTableRow.isQueryTableEntityChecked}
|
||||
selectedOperator={queryTableRow.selectedOperator}
|
||||
selectedField={queryTableRow.selectedField}
|
||||
selectedOperation={queryTableRow.selectedOperation}
|
||||
opertationOptions={queryTableRow.opertionOptions}
|
||||
entityValue={queryTableRow.entityValue}
|
||||
selectedEntityType={queryTableRow.selectedEntityType}
|
||||
entityTypeOptions={queryTableRow.entityTypeOptions}
|
||||
fieldOptions={queryTableRow.fieldOptions}
|
||||
selectedTimestamp={queryTableRow.selectedTimestamp}
|
||||
timestampOptions={queryTableRow.timestampOptions}
|
||||
opertorOptions={queryTableRow.opertorOptions}
|
||||
isTimeStampSelected={queryTableRow.isTimeStampSelected}
|
||||
onAddNewClause={this.onAddNewClause}
|
||||
onDeleteClause={() => this.onDeleteClause(index)}
|
||||
onQueryTableEntityCheck={() => this.onQueryTableEntityCheck(index)}
|
||||
onEntityValueChange={(_event, newInput?: string) => this.onEntityValueChange(newInput, index)}
|
||||
onDropdownChange={(selectedOption: IDropdownOption, selectedOptionType: string) =>
|
||||
this.onDropdownChange(selectedOption, selectedOptionType, index)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
<div
|
||||
className="addClause"
|
||||
role="button"
|
||||
onClick={this.state.queryViewModel.queryBuilderViewModel().addNewClause}
|
||||
// data-bind="click: addNewClause, event: { keydown: onAddNewClauseKeyDown }, attr: { title: addNewClauseLine }"
|
||||
onClick={this.onAddNewClause}
|
||||
// onClick={this.state.queryViewModel.queryBuilderViewModel().addNewClause}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="addClause-heading">
|
||||
|
@ -1,13 +1,13 @@
|
||||
<!DOCTYPE html public "-//W3C//DTD HTML 4.0//en">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="height=device-height, width=device-width, initial-scale=1.0" />
|
||||
|
||||
<title>Azure Cosmos DB Emulator</title>
|
||||
|
||||
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon" />
|
||||
</head>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="height=device-height, width=device-width, initial-scale=1.0" />
|
||||
|
||||
<title>Azure Cosmos DB Emulator</title>
|
||||
|
||||
<link rel="shortcut icon" href="images/favicon.ico" type="image/x-icon" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
|
Loading…
x
Reference in New Issue
Block a user