mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
Add close method to ModelView dashboards (#14812)
* Add close method to ModelView dashboards * fix closing * remove accessors * Update errors
This commit is contained in:
@@ -63,7 +63,7 @@ describe('postgresOverviewPage', () => {
|
||||
|
||||
// Setup the PostgresOverviewPage
|
||||
const { modelViewMock } = createModelViewMock();
|
||||
postgresOverview = new PostgresOverviewPage(modelViewMock.object, controllerModel, postgresModel);
|
||||
postgresOverview = new PostgresOverviewPage(modelViewMock.object, undefined!, controllerModel, postgresModel);
|
||||
// Call the getter to initialize toolbar, but we don't need to use it for anything
|
||||
// eslint-disable-next-line code-no-unused-expressions
|
||||
postgresOverview['toolbarContainer'];
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as azdata from 'azdata';
|
||||
|
||||
export abstract class Dashboard {
|
||||
|
||||
private dashboard!: azdata.window.ModelViewDashboard;
|
||||
protected dashboard!: azdata.window.ModelViewDashboard;
|
||||
|
||||
constructor(protected title: string, protected readonly name: string) { }
|
||||
|
||||
@@ -16,6 +16,10 @@ export abstract class Dashboard {
|
||||
await this.dashboard.open();
|
||||
}
|
||||
|
||||
public async closeDashboard(): Promise<void> {
|
||||
await this.dashboard.close();
|
||||
}
|
||||
|
||||
protected createDashboard(): azdata.window.ModelViewDashboard {
|
||||
const dashboard = azdata.window.createModelViewDashboard(this.title, this.name);
|
||||
dashboard.registerTabs(async modelView => {
|
||||
|
||||
@@ -11,7 +11,7 @@ export abstract class DashboardPage extends InitializingComponent {
|
||||
|
||||
protected disposables: vscode.Disposable[] = [];
|
||||
|
||||
constructor(protected modelView: azdata.ModelView) {
|
||||
constructor(protected modelView: azdata.ModelView, protected dashboard: azdata.window.ModelViewDashboard) {
|
||||
super();
|
||||
this.disposables.push(modelView.onClosed(() => {
|
||||
// Clean up best we can
|
||||
|
||||
@@ -22,7 +22,7 @@ export class ControllerDashboard extends Dashboard {
|
||||
}
|
||||
|
||||
protected async registerTabs(modelView: azdata.ModelView): Promise<(azdata.DashboardTab | azdata.DashboardTabGroup)[]> {
|
||||
const overviewPage = new ControllerDashboardOverviewPage(modelView, this._controllerModel);
|
||||
const overviewPage = new ControllerDashboardOverviewPage(modelView, this.dashboard, this._controllerModel);
|
||||
return [
|
||||
overviewPage.tab
|
||||
];
|
||||
|
||||
@@ -35,8 +35,8 @@ export class ControllerDashboardOverviewPage extends DashboardPage {
|
||||
instanceNamespace: '-',
|
||||
};
|
||||
|
||||
constructor(modelView: azdata.ModelView, private _controllerModel: ControllerModel) {
|
||||
super(modelView);
|
||||
constructor(modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _controllerModel: ControllerModel) {
|
||||
super(modelView, dashboard);
|
||||
|
||||
this._azurecoreApi = vscode.extensions.getExtension(azurecore.extension.name)?.exports;
|
||||
|
||||
|
||||
@@ -32,8 +32,8 @@ export class MiaaComputeAndStoragePage extends DashboardPage {
|
||||
|
||||
private readonly _azdataApi: azdataExt.IExtension;
|
||||
|
||||
constructor(protected modelView: azdata.ModelView, private _miaaModel: MiaaModel) {
|
||||
super(modelView);
|
||||
constructor(protected modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _miaaModel: MiaaModel) {
|
||||
super(modelView, dashboard);
|
||||
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
|
||||
|
||||
this.initializeConfigurationBoxes();
|
||||
|
||||
@@ -16,8 +16,8 @@ export class MiaaConnectionStringsPage extends DashboardPage {
|
||||
private _keyValueContainer!: KeyValueContainer;
|
||||
private _connectionStringsMessage!: azdata.TextComponent;
|
||||
|
||||
constructor(modelView: azdata.ModelView, private _miaaModel: MiaaModel) {
|
||||
super(modelView);
|
||||
constructor(modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _miaaModel: MiaaModel) {
|
||||
super(modelView, dashboard);
|
||||
this.disposables.push(this._miaaModel.onConfigUpdated(_ =>
|
||||
this.eventuallyRunOnInitialized(() => this.updateConnectionStrings())));
|
||||
}
|
||||
|
||||
@@ -26,9 +26,9 @@ export class MiaaDashboard extends Dashboard {
|
||||
}
|
||||
|
||||
protected async registerTabs(modelView: azdata.ModelView): Promise<(azdata.DashboardTab | azdata.DashboardTabGroup)[]> {
|
||||
const overviewPage = new MiaaDashboardOverviewPage(modelView, this._controllerModel, this._miaaModel);
|
||||
const connectionStringsPage = new MiaaConnectionStringsPage(modelView, this._miaaModel);
|
||||
const computeAndStoragePage = new MiaaComputeAndStoragePage(modelView, this._miaaModel);
|
||||
const overviewPage = new MiaaDashboardOverviewPage(modelView, this.dashboard, this._controllerModel, this._miaaModel);
|
||||
const connectionStringsPage = new MiaaConnectionStringsPage(modelView, this.dashboard, this._miaaModel);
|
||||
const computeAndStoragePage = new MiaaComputeAndStoragePage(modelView, this.dashboard, this._miaaModel);
|
||||
return [
|
||||
overviewPage.tab,
|
||||
{
|
||||
|
||||
@@ -48,8 +48,8 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
|
||||
vCores: ''
|
||||
};
|
||||
|
||||
constructor(modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _miaaModel: MiaaModel) {
|
||||
super(modelView);
|
||||
constructor(modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _controllerModel: ControllerModel, private _miaaModel: MiaaModel) {
|
||||
super(modelView, dashboard);
|
||||
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
|
||||
this._azurecoreApi = vscode.extensions.getExtension(azurecore.extension.name)?.exports;
|
||||
|
||||
@@ -250,11 +250,17 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
|
||||
} finally {
|
||||
session.dispose();
|
||||
}
|
||||
|
||||
}
|
||||
);
|
||||
await this._controllerModel.refreshTreeNode();
|
||||
vscode.window.showInformationMessage(loc.instanceDeleted(this._miaaModel.info.name));
|
||||
try {
|
||||
await this.dashboard.close();
|
||||
} catch (err) {
|
||||
// Failures closing the dashboard aren't something we need to show users
|
||||
console.log('Error closing MIAA dashboard ', err);
|
||||
}
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(loc.instanceDeletionFailed(this._miaaModel.info.name, error));
|
||||
|
||||
@@ -47,8 +47,8 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
|
||||
|
||||
private readonly _azdataApi: azdataExt.IExtension;
|
||||
|
||||
constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) {
|
||||
super(modelView);
|
||||
constructor(protected modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _postgresModel: PostgresModel) {
|
||||
super(modelView, dashboard);
|
||||
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
|
||||
|
||||
this.initializeConfigurationBoxes();
|
||||
|
||||
@@ -14,8 +14,8 @@ export class PostgresConnectionStringsPage extends DashboardPage {
|
||||
private keyValueContainer?: KeyValueContainer;
|
||||
private connectionStringsLoading!: azdata.LoadingComponent;
|
||||
|
||||
constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) {
|
||||
super(modelView);
|
||||
constructor(protected modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _postgresModel: PostgresModel) {
|
||||
super(modelView, dashboard);
|
||||
|
||||
this.disposables.push(this._postgresModel.onConfigUpdated(
|
||||
() => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated())));
|
||||
|
||||
@@ -11,8 +11,8 @@ import { PostgresModel } from '../../../models/postgresModel';
|
||||
|
||||
export class PostgresCoordinatorNodeParametersPage extends PostgresParametersPage {
|
||||
|
||||
constructor(protected modelView: azdata.ModelView, _postgresModel: PostgresModel) {
|
||||
super(modelView, _postgresModel);
|
||||
constructor(modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, postgresModel: PostgresModel) {
|
||||
super(modelView, dashboard, postgresModel);
|
||||
}
|
||||
|
||||
protected get title(): string {
|
||||
|
||||
@@ -32,16 +32,16 @@ export class PostgresDashboard extends Dashboard {
|
||||
}
|
||||
|
||||
protected async registerTabs(modelView: azdata.ModelView): Promise<(azdata.DashboardTab | azdata.DashboardTabGroup)[]> {
|
||||
const overviewPage = new PostgresOverviewPage(modelView, this._controllerModel, this._postgresModel);
|
||||
const connectionStringsPage = new PostgresConnectionStringsPage(modelView, this._postgresModel);
|
||||
const computeAndStoragePage = new PostgresComputeAndStoragePage(modelView, this._postgresModel);
|
||||
const propertiesPage = new PostgresPropertiesPage(modelView, this._controllerModel, this._postgresModel);
|
||||
const overviewPage = new PostgresOverviewPage(modelView, this.dashboard, this._controllerModel, this._postgresModel);
|
||||
const connectionStringsPage = new PostgresConnectionStringsPage(modelView, this.dashboard, this._postgresModel);
|
||||
const computeAndStoragePage = new PostgresComputeAndStoragePage(modelView, this.dashboard, this._postgresModel);
|
||||
const propertiesPage = new PostgresPropertiesPage(modelView, this.dashboard, this._controllerModel, this._postgresModel);
|
||||
// TODO Add dashboard once backend is able to be connected for per role server parameter edits.
|
||||
// const coordinatorNodeParametersPage = new PostgresCoordinatorNodeParametersPage(modelView, this._postgresModel);
|
||||
const workerNodeParametersPage = new PostgresWorkerNodeParametersPage(modelView, this._postgresModel);
|
||||
const diagnoseAndSolveProblemsPage = new PostgresDiagnoseAndSolveProblemsPage(modelView, this._context, this._postgresModel);
|
||||
const supportRequestPage = new PostgresSupportRequestPage(modelView, this._controllerModel, this._postgresModel);
|
||||
const resourceHealthPage = new PostgresResourceHealthPage(modelView, this._postgresModel);
|
||||
const workerNodeParametersPage = new PostgresWorkerNodeParametersPage(modelView, this.dashboard, this._postgresModel);
|
||||
const diagnoseAndSolveProblemsPage = new PostgresDiagnoseAndSolveProblemsPage(modelView, this.dashboard, this._context, this._postgresModel);
|
||||
const supportRequestPage = new PostgresSupportRequestPage(modelView, this.dashboard, this._controllerModel, this._postgresModel);
|
||||
const resourceHealthPage = new PostgresResourceHealthPage(modelView, this.dashboard, this._postgresModel);
|
||||
|
||||
return [
|
||||
overviewPage.tab,
|
||||
|
||||
@@ -11,8 +11,8 @@ import { DashboardPage } from '../../components/dashboardPage';
|
||||
import { PostgresModel } from '../../../models/postgresModel';
|
||||
|
||||
export class PostgresDiagnoseAndSolveProblemsPage extends DashboardPage {
|
||||
constructor(protected modelView: azdata.ModelView, private _context: vscode.ExtensionContext, private _postgresModel: PostgresModel) {
|
||||
super(modelView);
|
||||
constructor(protected modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _context: vscode.ExtensionContext, private _postgresModel: PostgresModel) {
|
||||
super(modelView, dashboard);
|
||||
}
|
||||
|
||||
protected get title(): string {
|
||||
|
||||
@@ -37,8 +37,8 @@ export class PostgresOverviewPage extends DashboardPage {
|
||||
|
||||
private readonly _azdataApi: azdataExt.IExtension;
|
||||
|
||||
constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
|
||||
super(modelView);
|
||||
constructor(protected modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
|
||||
super(modelView, dashboard);
|
||||
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
|
||||
|
||||
this.disposables.push(
|
||||
@@ -270,6 +270,13 @@ export class PostgresOverviewPage extends DashboardPage {
|
||||
);
|
||||
await this._controllerModel.refreshTreeNode();
|
||||
vscode.window.showInformationMessage(loc.instanceDeleted(this._postgresModel.info.name));
|
||||
try {
|
||||
await this.dashboard.close();
|
||||
} catch (err) {
|
||||
// Failures closing the dashboard aren't something we need to show users
|
||||
console.log('Error closing Arc Postgres dashboard ', err);
|
||||
}
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(loc.instanceDeletionFailed(this._postgresModel.info.name, error));
|
||||
|
||||
@@ -36,8 +36,8 @@ export abstract class PostgresParametersPage extends DashboardPage {
|
||||
|
||||
protected readonly _azdataApi: azdataExt.IExtension;
|
||||
|
||||
constructor(protected modelView: azdata.ModelView, protected _postgresModel: PostgresModel) {
|
||||
super(modelView);
|
||||
constructor(protected modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, protected _postgresModel: PostgresModel) {
|
||||
super(modelView, dashboard);
|
||||
|
||||
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@ export class PostgresPropertiesPage extends DashboardPage {
|
||||
private loading?: azdata.LoadingComponent;
|
||||
private keyValueContainer?: KeyValueContainer;
|
||||
|
||||
constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
|
||||
super(modelView);
|
||||
constructor(protected modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
|
||||
super(modelView, dashboard);
|
||||
|
||||
this.disposables.push(this._postgresModel.onConfigUpdated(
|
||||
() => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated())));
|
||||
|
||||
@@ -36,8 +36,8 @@ export class PostgresResourceHealthPage extends DashboardPage {
|
||||
private coordinatorData: PodHealthModel[] = [];
|
||||
private podsData: PodHealthModel[] = [];
|
||||
|
||||
constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) {
|
||||
super(modelView);
|
||||
constructor(protected modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _postgresModel: PostgresModel) {
|
||||
super(modelView, dashboard);
|
||||
|
||||
this.disposables.push(
|
||||
this._postgresModel.onConfigUpdated(() => this.eventuallyRunOnInitialized(() => this.handleConfigUpdated())));
|
||||
|
||||
@@ -13,8 +13,8 @@ import { ResourceType } from 'arc';
|
||||
import { PostgresModel } from '../../../models/postgresModel';
|
||||
|
||||
export class PostgresSupportRequestPage extends DashboardPage {
|
||||
constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
|
||||
super(modelView);
|
||||
constructor(protected modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
|
||||
super(modelView, dashboard);
|
||||
}
|
||||
|
||||
protected get title(): string {
|
||||
|
||||
@@ -12,8 +12,8 @@ import { PostgresModel } from '../../../models/postgresModel';
|
||||
|
||||
export class PostgresWorkerNodeParametersPage extends PostgresParametersPage {
|
||||
|
||||
constructor(protected modelView: azdata.ModelView, _postgresModel: PostgresModel) {
|
||||
super(modelView, _postgresModel);
|
||||
constructor(modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, postgresModel: PostgresModel) {
|
||||
super(modelView, dashboard, postgresModel);
|
||||
}
|
||||
|
||||
protected get title(): string {
|
||||
|
||||
1
src/sql/azdata.proposed.d.ts
vendored
1
src/sql/azdata.proposed.d.ts
vendored
@@ -525,6 +525,7 @@ declare module 'azdata' {
|
||||
export interface ModelViewDashboard {
|
||||
registerTabs(handler: (view: ModelView) => Thenable<(DashboardTab | DashboardTabGroup)[]>): void;
|
||||
open(): Thenable<void>;
|
||||
close(): Thenable<void>;
|
||||
updateTabs(tabs: (DashboardTab | DashboardTabGroup)[]): void;
|
||||
selectTab(id: string): void;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import * as azdata from 'azdata';
|
||||
import { assign } from 'vs/base/common/objects';
|
||||
import { TelemetryView, TelemetryAction } from 'sql/platform/telemetry/common/telemetryKeys';
|
||||
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
|
||||
import { IEditorInput, IEditorPane } from 'vs/workbench/common/editor';
|
||||
|
||||
@extHostNamedCustomer(SqlMainContext.MainThreadModelViewDialog)
|
||||
export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape {
|
||||
@@ -30,6 +31,7 @@ export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape
|
||||
private readonly _wizardPageHandles = new Map<WizardPage, number>();
|
||||
private readonly _wizards = new Map<number, Wizard>();
|
||||
private readonly _editorInputModels = new Map<number, ModelViewInputModel>();
|
||||
private readonly _editors = new Map<number, { pane: IEditorPane, input: IEditorInput }>();
|
||||
private _dialogService: CustomDialogService;
|
||||
|
||||
constructor(
|
||||
@@ -58,8 +60,15 @@ export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape
|
||||
this._telemetryService.createActionEvent(TelemetryView.Shell, TelemetryAction.ModelViewDashboardOpened)
|
||||
.withAdditionalProperties({ name: name })
|
||||
.send();
|
||||
this._editorService.openEditor(input, editorOptions, position as any).then((editor) => {
|
||||
this._editorService.openEditor(input, editorOptions, position as any).then((editorPane) => {
|
||||
this._editorInputModels.set(handle, model);
|
||||
this._editors.set(handle, { pane: editorPane, input: editorPane.input });
|
||||
const disposable = this._editorService.onDidCloseEditor(e => {
|
||||
if (e.editor === input) {
|
||||
this._editors.delete(handle);
|
||||
disposable.dispose();
|
||||
}
|
||||
});
|
||||
resolve();
|
||||
}, error => {
|
||||
reject(error);
|
||||
@@ -67,6 +76,18 @@ export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape
|
||||
});
|
||||
}
|
||||
|
||||
public $closeEditor(handle: number): Thenable<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const editor = this._editors.get(handle);
|
||||
if (editor) {
|
||||
editor.pane.group.closeEditor(editor.input).then(() => {
|
||||
resolve();
|
||||
}).catch(e => reject(e));
|
||||
}
|
||||
reject(new Error(`Could not find editor with handle ${handle}`));
|
||||
});
|
||||
}
|
||||
|
||||
private handleSave(handle: number): Thenable<boolean> {
|
||||
return this._proxy.$handleSave(handle);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ const PREVIOUS_LABEL = nls.localize('dialogPreviousLabel', "Previous");
|
||||
|
||||
class ModelViewPanelImpl implements azdata.window.ModelViewPanel {
|
||||
private _modelView: azdata.ModelView;
|
||||
private _handle: number;
|
||||
public handle: number;
|
||||
protected _modelViewId: string;
|
||||
protected _valid: boolean = true;
|
||||
protected _onValidityChanged: vscode.Event<boolean>;
|
||||
@@ -38,7 +38,7 @@ class ModelViewPanelImpl implements azdata.window.ModelViewPanel {
|
||||
|
||||
public registerContent(handler: (view: azdata.ModelView) => Thenable<void>): void {
|
||||
if (!this._modelViewId) {
|
||||
let viewId = this._viewType + this._handle;
|
||||
let viewId = this._viewType + this.handle;
|
||||
this.setModelViewId(viewId);
|
||||
this._extHostModelView.$registerProvider(viewId, modelView => {
|
||||
this._modelView = modelView;
|
||||
@@ -47,10 +47,6 @@ class ModelViewPanelImpl implements azdata.window.ModelViewPanel {
|
||||
}
|
||||
}
|
||||
|
||||
public set handle(value: number) {
|
||||
this._handle = value;
|
||||
}
|
||||
|
||||
public setModelViewId(value: string) {
|
||||
this._modelViewId = value;
|
||||
}
|
||||
@@ -93,6 +89,10 @@ class ModelViewEditorImpl extends ModelViewPanelImpl implements azdata.workspace
|
||||
return this._proxy.$openEditor(this.handle, this._modelViewId, this._title, this._name, this._options, position);
|
||||
}
|
||||
|
||||
public closeEditor(): Thenable<void> {
|
||||
return this._proxy.$closeEditor(this.handle);
|
||||
}
|
||||
|
||||
public get isDirty(): boolean {
|
||||
return this._isDirty;
|
||||
}
|
||||
@@ -568,6 +568,10 @@ class ModelViewDashboardImpl implements azdata.window.ModelViewDashboard {
|
||||
return this._editor.openEditor();
|
||||
}
|
||||
|
||||
close(): Thenable<void> {
|
||||
return this._editor.closeEditor();
|
||||
}
|
||||
|
||||
createTab(tab: azdata.DashboardTab, view: azdata.ModelView): azdata.Tab {
|
||||
if (tab.toolbar) {
|
||||
const flexContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
|
||||
|
||||
@@ -820,6 +820,7 @@ export interface ExtHostModelViewDialogShape {
|
||||
|
||||
export interface MainThreadModelViewDialogShape extends IDisposable {
|
||||
$openEditor(handle: number, modelViewId: string, title: string, name?: string, options?: azdata.ModelViewEditorOptions, position?: vscode.ViewColumn): Thenable<void>;
|
||||
$closeEditor(handle: number): Thenable<void>;
|
||||
$openDialog(handle: number, dialogName?: string): Thenable<void>;
|
||||
$closeDialog(handle: number): Thenable<void>;
|
||||
$setDialogDetails(handle: number, details: IModelViewDialogDetails): Thenable<void>;
|
||||
|
||||
Reference in New Issue
Block a user