mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-11 02:32:35 -05:00
Azure Arc extension (#10400)
Adds an extension for Azure Arc with some initial Postgres pages
This commit is contained in:
28
extensions/arc/src/ui/components/dashboard.ts
Normal file
28
extensions/arc/src/ui/components/dashboard.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
|
||||
export abstract class Dashboard {
|
||||
|
||||
private dashboard!: azdata.window.ModelViewDashboard;
|
||||
|
||||
constructor(protected title: string) { }
|
||||
|
||||
public async showDashboard(): Promise<void> {
|
||||
this.dashboard = this.createDashboard();
|
||||
await this.dashboard.open();
|
||||
}
|
||||
|
||||
protected createDashboard(): azdata.window.ModelViewDashboard {
|
||||
const dashboard = azdata.window.createModelViewDashboard(this.title);
|
||||
dashboard.registerTabs(async modelView => {
|
||||
return await this.registerTabs(modelView);
|
||||
});
|
||||
return dashboard;
|
||||
}
|
||||
|
||||
protected abstract async registerTabs(modelView: azdata.ModelView): Promise<(azdata.DashboardTab | azdata.DashboardTabGroup)[]>;
|
||||
}
|
||||
32
extensions/arc/src/ui/components/dashboardPage.ts
Normal file
32
extensions/arc/src/ui/components/dashboardPage.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import { InitializingComponent } from './initializingComponent';
|
||||
|
||||
export abstract class DashboardPage extends InitializingComponent {
|
||||
|
||||
constructor(protected modelView: azdata.ModelView) {
|
||||
super();
|
||||
}
|
||||
|
||||
public get tab(): azdata.DashboardTab {
|
||||
return {
|
||||
title: this.title,
|
||||
id: this.id,
|
||||
icon: this.icon,
|
||||
content: this.container,
|
||||
toolbar: this.toolbarContainer
|
||||
};
|
||||
}
|
||||
|
||||
protected abstract get title(): string;
|
||||
protected abstract get id(): string;
|
||||
protected abstract get icon(): { dark: string; light: string; };
|
||||
protected abstract get container(): azdata.Component;
|
||||
protected abstract get toolbarContainer(): azdata.ToolbarContainer;
|
||||
|
||||
}
|
||||
|
||||
40
extensions/arc/src/ui/components/initializingComponent.ts
Normal file
40
extensions/arc/src/ui/components/initializingComponent.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Deferred } from '../../common/promise';
|
||||
|
||||
export abstract class InitializingComponent {
|
||||
|
||||
private _initialized: boolean = false;
|
||||
|
||||
private onInitializedPromise: Deferred<void> = new Deferred();
|
||||
|
||||
constructor() { }
|
||||
|
||||
protected get initialized(): boolean {
|
||||
return this._initialized;
|
||||
}
|
||||
|
||||
protected set initialized(value: boolean) {
|
||||
if (!this._initialized && value) {
|
||||
this._initialized = true;
|
||||
this.onInitializedPromise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the specified action when the component is initialized. If already initialized just runs
|
||||
* the action immediately.
|
||||
* @param action The action to be ran when the page is initialized
|
||||
*/
|
||||
protected eventuallyRunOnInitialized(action: () => void): void {
|
||||
if (!this._initialized) {
|
||||
this.onInitializedPromise.promise.then(() => action()).catch(error => console.error(`Unexpected error running onInitialized action: ${error}`));
|
||||
} else {
|
||||
action();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
96
extensions/arc/src/ui/components/keyValueContainer.ts
Normal file
96
extensions/arc/src/ui/components/keyValueContainer.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as azdata from 'azdata';
|
||||
import * as loc from '../../localizedConstants';
|
||||
import { IconPathHelper, cssStyles } from '../../constants';
|
||||
|
||||
/** A container with a single vertical column of KeyValue pairs */
|
||||
export class KeyValueContainer {
|
||||
public container: azdata.DivContainer;
|
||||
|
||||
constructor(private modelBuilder: azdata.ModelBuilder, pairs: KeyValue[]) {
|
||||
this.container = modelBuilder.divContainer().component();
|
||||
this.refresh(pairs);
|
||||
}
|
||||
|
||||
public refresh(pairs: KeyValue[]) {
|
||||
this.container.clearItems();
|
||||
this.container.addItems(
|
||||
pairs.map(p => p.getComponent(this.modelBuilder)),
|
||||
{ CSSStyles: { 'margin-bottom': '15px', 'min-height': '30px' } }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** A key value pair in the KeyValueContainer */
|
||||
export abstract class KeyValue {
|
||||
constructor(protected key: string, protected value: string) { }
|
||||
|
||||
/** Returns a component representing the entire KeyValue pair */
|
||||
public getComponent(modelBuilder: azdata.ModelBuilder) {
|
||||
const container = modelBuilder.flexContainer().withLayout({ flexWrap: 'wrap', alignItems: 'center' }).component();
|
||||
const key = modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
value: this.key,
|
||||
CSSStyles: { ...cssStyles.text, 'font-weight': 'bold', 'margin-block-start': '0px', 'margin-block-end': '0px' }
|
||||
}).component();
|
||||
|
||||
container.addItem(key, { flex: `0 0 200px` });
|
||||
container.addItem(this.getValueComponent(modelBuilder), { flex: '1 1 250px' });
|
||||
return container;
|
||||
}
|
||||
|
||||
/** Returns a component representing the value of the KeyValue pair */
|
||||
protected abstract getValueComponent(modelBuilder: azdata.ModelBuilder): azdata.Component;
|
||||
}
|
||||
|
||||
/** Implementation of KeyValue where the value is text */
|
||||
export class TextKeyValue extends KeyValue {
|
||||
getValueComponent(modelBuilder: azdata.ModelBuilder): azdata.Component {
|
||||
return modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
value: this.value,
|
||||
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
|
||||
}).component();
|
||||
}
|
||||
}
|
||||
|
||||
/** Implementation of KeyValue where the value is a readonly copyable input field */
|
||||
export class InputKeyValue extends KeyValue {
|
||||
getValueComponent(modelBuilder: azdata.ModelBuilder): azdata.Component {
|
||||
const container = modelBuilder.flexContainer().withLayout({ alignItems: 'center' }).component();
|
||||
container.addItem(modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
|
||||
value: this.value // TODO: Add a readOnly property to input boxes
|
||||
}).component());
|
||||
|
||||
const copy = modelBuilder.button().withProperties<azdata.ButtonProperties>({
|
||||
iconPath: IconPathHelper.copy, width: '17px', height: '17px'
|
||||
}).component();
|
||||
|
||||
copy.onDidClick(async () => {
|
||||
vscode.env.clipboard.writeText(this.value);
|
||||
vscode.window.showInformationMessage(loc.copiedToClipboard(this.key));
|
||||
});
|
||||
|
||||
container.addItem(copy, { CSSStyles: { 'margin-left': '10px' } });
|
||||
return container;
|
||||
}
|
||||
}
|
||||
|
||||
/** Implementation of KeyValue where the value is a clickable link */
|
||||
export class LinkKeyValue extends KeyValue {
|
||||
constructor(key: string, value: string, private onClick: (e: any) => any) {
|
||||
super(key, value);
|
||||
}
|
||||
|
||||
getValueComponent(modelBuilder: azdata.ModelBuilder): azdata.Component {
|
||||
const link = modelBuilder.hyperlink().withProperties<azdata.HyperlinkComponentProperties>({
|
||||
label: this.value, url: ''
|
||||
}).component();
|
||||
|
||||
link.onDidClick(this.onClick);
|
||||
return link;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as loc from '../../../localizedConstants';
|
||||
import { IconPathHelper } from '../../../constants';
|
||||
import { PostgresDashboardPage } from './postgresDashboardPage';
|
||||
|
||||
export class PostgresBackupPage extends PostgresDashboardPage {
|
||||
protected get title(): string {
|
||||
return loc.backup;
|
||||
}
|
||||
|
||||
protected get id(): string {
|
||||
return 'postgres-backup';
|
||||
}
|
||||
|
||||
protected get icon(): { dark: string; light: string; } {
|
||||
return IconPathHelper.backup;
|
||||
}
|
||||
|
||||
protected get container(): azdata.Component {
|
||||
return this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: loc.backup }).component();
|
||||
}
|
||||
|
||||
protected get toolbarContainer(): azdata.ToolbarContainer {
|
||||
return this.modelView.modelBuilder.toolbarContainer().component();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as loc from '../../../localizedConstants';
|
||||
import { IconPathHelper } from '../../../constants';
|
||||
import { PostgresDashboardPage } from './postgresDashboardPage';
|
||||
|
||||
export class PostgresComputeStoragePage extends PostgresDashboardPage {
|
||||
protected get title(): string {
|
||||
return loc.computeAndStorage;
|
||||
}
|
||||
|
||||
protected get id(): string {
|
||||
return 'postgres-compute-storage';
|
||||
}
|
||||
|
||||
protected get icon(): { dark: string; light: string; } {
|
||||
return IconPathHelper.computeStorage;
|
||||
}
|
||||
|
||||
protected get container(): azdata.Component {
|
||||
return this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: loc.computeAndStorage }).component();
|
||||
}
|
||||
|
||||
protected get toolbarContainer(): azdata.ToolbarContainer {
|
||||
return this.modelView.modelBuilder.toolbarContainer().component();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as loc from '../../../localizedConstants';
|
||||
import { IconPathHelper, cssStyles } from '../../../constants';
|
||||
import { PostgresDashboardPage } from './postgresDashboardPage';
|
||||
import { KeyValueContainer, KeyValue, InputKeyValue } from '../../components/keyValueContainer';
|
||||
|
||||
export class PostgresConnectionStringsPage extends PostgresDashboardPage {
|
||||
protected get title(): string {
|
||||
return loc.connectionStrings;
|
||||
}
|
||||
|
||||
protected get id(): string {
|
||||
return 'postgres-connection-strings';
|
||||
}
|
||||
|
||||
protected get icon(): { dark: string; light: string; } {
|
||||
return IconPathHelper.connection;
|
||||
}
|
||||
|
||||
protected get container(): azdata.Component {
|
||||
const root = this.modelView.modelBuilder.divContainer().component();
|
||||
const content = this.modelView.modelBuilder.divContainer().component();
|
||||
root.addItem(content, { CSSStyles: { 'margin': '20px' } });
|
||||
|
||||
content.addItem(this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
value: loc.connectionStrings,
|
||||
CSSStyles: { ...cssStyles.title }
|
||||
}).component());
|
||||
|
||||
const info = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
value: `${loc.selectConnectionString}. `,
|
||||
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
|
||||
}).component();
|
||||
|
||||
const link = this.modelView.modelBuilder.hyperlink().withProperties<azdata.HyperlinkComponentProperties>({
|
||||
label: loc.learnAboutPostgresClients,
|
||||
url: 'http://example.com', // TODO link to documentation
|
||||
}).component();
|
||||
|
||||
content.addItem(
|
||||
this.modelView.modelBuilder.flexContainer().withItems([info, link]).withLayout({ flexWrap: 'wrap' }).component(),
|
||||
{ CSSStyles: { display: 'inline-flex', 'margin-bottom': '25px' } });
|
||||
|
||||
const endpoint: { ip?: string, port?: number } = this.databaseModel.endpoint();
|
||||
const password = this.databaseModel.password();
|
||||
|
||||
const pairs: KeyValue[] = [
|
||||
new InputKeyValue('ADO.NET', `Server=${endpoint.ip};Database=postgres;Port=${endpoint.port};User Id=postgres;Password=${password};Ssl Mode=Require;`),
|
||||
new InputKeyValue('C++ (libpq)', `host=${endpoint.ip} port=${endpoint.port} dbname=postgres user=postgres password=${password} sslmode=require`),
|
||||
new InputKeyValue('JDBC', `jdbc:postgresql://${endpoint.ip}:${endpoint.port}/postgres?user=postgres&password=${password}&sslmode=require`),
|
||||
new InputKeyValue('Node.js', `host=${endpoint.ip} port=${endpoint.port} dbname=postgres user=postgres password=${password} sslmode=require`),
|
||||
new InputKeyValue('PHP', `host=${endpoint.ip} port=${endpoint.port} dbname=postgres user=postgres password=${password} sslmode=require`),
|
||||
new InputKeyValue('psql', `psql "host=${endpoint.ip} port=${endpoint.port} dbname=postgres user=postgres password=${password} sslmode=require`),
|
||||
new InputKeyValue('Python', `dbname='postgres' user='postgres' host='${endpoint.ip}' password='${password}' port='${endpoint.port}' sslmode='true'`),
|
||||
new InputKeyValue('Ruby', `host=${endpoint.ip}; dbname=postgres user=postgres password=${password} port=${endpoint.port} sslmode=require`),
|
||||
new InputKeyValue('Web App', `Database=postgres; Data Source=${endpoint.ip}; User Id=postgres; Password=${password}`)
|
||||
];
|
||||
|
||||
const keyValueContainer = new KeyValueContainer(this.modelView.modelBuilder, pairs);
|
||||
content.addItem(keyValueContainer.container);
|
||||
return root;
|
||||
}
|
||||
|
||||
protected get toolbarContainer(): azdata.ToolbarContainer {
|
||||
return this.modelView.modelBuilder.toolbarContainer().component();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as loc from '../../../localizedConstants';
|
||||
import { ControllerModel } from '../../../models/controllerModel';
|
||||
import { PostgresModel } from '../../../models/postgresModel';
|
||||
import { PostgresOverviewPage } from './postgresOverviewPage';
|
||||
import { PostgresComputeStoragePage } from './postgresComputeStoragePage';
|
||||
import { PostgresConnectionStringsPage } from './postgresConnectionStringsPage';
|
||||
import { PostgresBackupPage } from './postgresBackupPage';
|
||||
import { PostgresPropertiesPage } from './postgresPropertiesPage';
|
||||
import { PostgresNetworkingPage } from './postgresNetworkingPage';
|
||||
import { Dashboard } from '../../components/dashboard';
|
||||
|
||||
export class PostgresDashboard extends Dashboard {
|
||||
constructor(title: string, private _controllerModel: ControllerModel, private _databaseModel: PostgresModel) {
|
||||
super(title);
|
||||
}
|
||||
|
||||
protected async registerTabs(modelView: azdata.ModelView): Promise<(azdata.DashboardTab | azdata.DashboardTabGroup)[]> {
|
||||
await Promise.all([this._controllerModel.refresh(), this._databaseModel.refresh()]);
|
||||
|
||||
const overviewPage = new PostgresOverviewPage(modelView, this._controllerModel, this._databaseModel);
|
||||
const computeStoragePage = new PostgresComputeStoragePage(modelView, this._controllerModel, this._databaseModel);
|
||||
const connectionStringsPage = new PostgresConnectionStringsPage(modelView, this._controllerModel, this._databaseModel);
|
||||
const backupPage = new PostgresBackupPage(modelView, this._controllerModel, this._databaseModel);
|
||||
const propertiesPage = new PostgresPropertiesPage(modelView, this._controllerModel, this._databaseModel);
|
||||
const networkingPage = new PostgresNetworkingPage(modelView, this._controllerModel, this._databaseModel);
|
||||
|
||||
return [
|
||||
overviewPage.tab,
|
||||
{
|
||||
title: loc.settings,
|
||||
tabs: [
|
||||
computeStoragePage.tab,
|
||||
connectionStringsPage.tab,
|
||||
backupPage.tab,
|
||||
propertiesPage.tab
|
||||
]
|
||||
}, {
|
||||
title: loc.security,
|
||||
tabs: [
|
||||
networkingPage.tab
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import { ControllerModel } from '../../../models/controllerModel';
|
||||
import { PostgresModel } from '../../../models/postgresModel';
|
||||
import { DashboardPage } from '../../components/dashboardPage';
|
||||
|
||||
export abstract class PostgresDashboardPage extends DashboardPage {
|
||||
constructor(protected modelView: azdata.ModelView, protected controllerModel: ControllerModel, protected databaseModel: PostgresModel) {
|
||||
super(modelView);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as loc from '../../../localizedConstants';
|
||||
import { IconPathHelper } from '../../../constants';
|
||||
import { PostgresDashboardPage } from './postgresDashboardPage';
|
||||
|
||||
export class PostgresNetworkingPage extends PostgresDashboardPage {
|
||||
protected get title(): string {
|
||||
return loc.networking;
|
||||
}
|
||||
|
||||
protected get id(): string {
|
||||
return 'postgres-networking';
|
||||
}
|
||||
|
||||
protected get icon(): { dark: string; light: string; } {
|
||||
return IconPathHelper.networking;
|
||||
}
|
||||
|
||||
protected get container(): azdata.Component {
|
||||
return this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: loc.networking }).component();
|
||||
}
|
||||
|
||||
protected get toolbarContainer(): azdata.ToolbarContainer {
|
||||
return this.modelView.modelBuilder.toolbarContainer().component();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as azdata from 'azdata';
|
||||
import * as loc from '../../../localizedConstants';
|
||||
import { IconPathHelper, cssStyles } from '../../../constants';
|
||||
import { DuskyObjectModelsDatabase, DuskyObjectModelsDatabaseServiceArcPayload } from '../../../controller/generated/dusky/api';
|
||||
import { PostgresDashboardPage } from './postgresDashboardPage';
|
||||
|
||||
export class PostgresOverviewPage extends PostgresDashboardPage {
|
||||
protected get title(): string {
|
||||
return loc.overview;
|
||||
}
|
||||
|
||||
protected get id(): string {
|
||||
return 'postgres-overview';
|
||||
}
|
||||
|
||||
protected get icon(): { dark: string; light: string; } {
|
||||
return IconPathHelper.postgres;
|
||||
}
|
||||
|
||||
protected get container(): azdata.Component {
|
||||
const root = this.modelView.modelBuilder.divContainer().component();
|
||||
const content = this.modelView.modelBuilder.divContainer().component();
|
||||
root.addItem(content, { CSSStyles: { 'margin': '10px 20px 0px 20px' } });
|
||||
|
||||
const registration = this.controllerModel.registration('postgresInstances', this.databaseModel.namespace(), this.databaseModel.name());
|
||||
const endpoint: { ip?: string, port?: number } = this.databaseModel.endpoint();
|
||||
const essentials = this.modelView.modelBuilder.propertiesContainer().withProperties<azdata.PropertiesContainerComponentProperties>({
|
||||
propertyItems: [
|
||||
{ displayName: loc.name, value: this.databaseModel.name() },
|
||||
{ displayName: loc.serverGroupType, value: loc.postgresArcProductName },
|
||||
{ displayName: loc.resourceGroup, value: registration?.resourceGroupName ?? 'None' },
|
||||
{ displayName: loc.coordinatorEndpoint, value: `postgresql://postgres:${this.databaseModel.password()}@${endpoint.ip}:${endpoint.port}` },
|
||||
{ displayName: loc.status, value: this.databaseModel.service().status?.state ?? '' },
|
||||
{ displayName: loc.postgresAdminUsername, value: 'postgres' },
|
||||
{ displayName: loc.dataController, value: this.controllerModel.namespace() },
|
||||
{ displayName: loc.nodeConfiguration, value: this.databaseModel.configuration() },
|
||||
{ displayName: loc.subscriptionId, value: registration?.subscriptionId ?? 'None' },
|
||||
{ displayName: loc.postgresVersion, value: this.databaseModel.service().spec.engine.version?.toString() ?? '' }
|
||||
]
|
||||
}).component();
|
||||
content.addItem(essentials, { CSSStyles: cssStyles.text });
|
||||
|
||||
// Service endpoints
|
||||
const titleCSS = { ...cssStyles.title, 'margin-block-start': '2em', 'margin-block-end': '0' };
|
||||
content.addItem(this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: loc.serviceEndpoints, CSSStyles: titleCSS }).component());
|
||||
|
||||
const kibanaQuery = `kubernetes_namespace:"${this.databaseModel.namespace()}" and cluster_name:"${this.databaseModel.name()}"`;
|
||||
const kibanaUrl = `${this.controllerModel.endpoint('logsui')?.endpoint}/app/kibana#/discover?_a=(query:(language:kuery,query:'${kibanaQuery}'))`;
|
||||
const grafanaUrl = `${this.controllerModel.endpoint('metricsui')?.endpoint}/d/postgres-metrics?var-Namespace=${this.databaseModel.namespace()}&var-Name=${this.databaseModel.name()}`;
|
||||
|
||||
const kibanaLink = this.modelView.modelBuilder.hyperlink().withProperties<azdata.HyperlinkComponentProperties>({ label: kibanaUrl, url: kibanaUrl, }).component();
|
||||
const grafanaLink = this.modelView.modelBuilder.hyperlink().withProperties<azdata.HyperlinkComponentProperties>({ label: grafanaUrl, url: grafanaUrl }).component();
|
||||
|
||||
const endpointsTable = this.modelView.modelBuilder.declarativeTable().withProperties<azdata.DeclarativeTableProperties>({
|
||||
width: '100%',
|
||||
columns: [
|
||||
{
|
||||
displayName: loc.name,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
isReadOnly: true,
|
||||
width: '20%',
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: cssStyles.tableRow
|
||||
},
|
||||
{
|
||||
displayName: loc.endpoint,
|
||||
valueType: azdata.DeclarativeDataType.component,
|
||||
isReadOnly: true,
|
||||
width: '50%',
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: {
|
||||
...cssStyles.tableRow,
|
||||
'overflow': 'hidden',
|
||||
'text-overflow': 'ellipsis',
|
||||
'white-space': 'nowrap',
|
||||
'max-width': '0'
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: loc.description,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
isReadOnly: true,
|
||||
width: '30%',
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: cssStyles.tableRow
|
||||
}
|
||||
],
|
||||
data: [
|
||||
[loc.kibanaDashboard, kibanaLink, loc.kibanaDashboardDescription],
|
||||
[loc.grafanaDashboard, grafanaLink, loc.grafanaDashboardDescription]]
|
||||
}).component();
|
||||
content.addItem(endpointsTable);
|
||||
|
||||
// Server group nodes
|
||||
content.addItem(this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: loc.serverGroupNodes, CSSStyles: titleCSS }).component());
|
||||
const nodesTable = this.modelView.modelBuilder.declarativeTable().withProperties<azdata.DeclarativeTableProperties>({
|
||||
width: '100%',
|
||||
columns: [
|
||||
{
|
||||
displayName: loc.name,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
isReadOnly: true,
|
||||
width: '30%',
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: cssStyles.tableRow
|
||||
},
|
||||
{
|
||||
displayName: loc.type,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
isReadOnly: true,
|
||||
width: '25%',
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: cssStyles.tableRow
|
||||
},
|
||||
{
|
||||
displayName: loc.fullyQualifiedDomain,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
isReadOnly: true,
|
||||
width: '45%',
|
||||
headerCssStyles: cssStyles.tableHeader,
|
||||
rowCssStyles: cssStyles.tableRow
|
||||
}
|
||||
],
|
||||
data: []
|
||||
}).component();
|
||||
|
||||
const nodes = this.databaseModel.numNodes();
|
||||
for (let i = 0; i < nodes; i++) {
|
||||
nodesTable.data.push([
|
||||
`${this.databaseModel.name()}-${i}`,
|
||||
i === 0 ? loc.coordinatorEndpoint : loc.worker,
|
||||
i === 0 ? `${endpoint.ip}:${endpoint.port}` : `${this.databaseModel.name()}-${i}.${this.databaseModel.name()}-svc.${this.databaseModel.namespace()}.svc.cluster.local`]);
|
||||
}
|
||||
|
||||
content.addItem(nodesTable, { CSSStyles: { 'margin-bottom': '20px' } });
|
||||
return root;
|
||||
}
|
||||
|
||||
protected get toolbarContainer(): azdata.ToolbarContainer {
|
||||
// New database
|
||||
const newDatabaseButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
|
||||
label: loc.newDatabase,
|
||||
iconPath: IconPathHelper.add
|
||||
}).component();
|
||||
|
||||
newDatabaseButton.onDidClick(async () => {
|
||||
const name = await vscode.window.showInputBox({ prompt: loc.databaseName });
|
||||
if (name === undefined) { return; }
|
||||
const db: DuskyObjectModelsDatabase = { name: name }; // TODO support other options (sharded, owner)
|
||||
try {
|
||||
await this.databaseModel.createDatabase(db);
|
||||
vscode.window.showInformationMessage(loc.databaseCreated(db.name));
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(loc.databaseCreationFailed(db.name, error));
|
||||
}
|
||||
});
|
||||
|
||||
// Reset password
|
||||
const resetPasswordButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
|
||||
label: loc.resetPassword,
|
||||
iconPath: IconPathHelper.edit
|
||||
}).component();
|
||||
|
||||
resetPasswordButton.onDidClick(async () => {
|
||||
const password = await vscode.window.showInputBox({ prompt: loc.newPassword, password: true });
|
||||
if (password === undefined) { return; }
|
||||
try {
|
||||
await this.databaseModel.update(s => {
|
||||
s.arc = s.arc ?? new DuskyObjectModelsDatabaseServiceArcPayload();
|
||||
s.arc.servicePassword = password;
|
||||
});
|
||||
vscode.window.showInformationMessage(loc.passwordReset(this.databaseModel.fullName()));
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(loc.passwordResetFailed(this.databaseModel.fullName(), error));
|
||||
}
|
||||
});
|
||||
|
||||
// Delete service
|
||||
const deleteButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
|
||||
label: loc.deleteText,
|
||||
iconPath: IconPathHelper.delete
|
||||
}).component();
|
||||
|
||||
deleteButton.onDidClick(async () => {
|
||||
const response = await vscode.window.showQuickPick([loc.yes, loc.no], {
|
||||
placeHolder: loc.deleteServicePrompt(this.databaseModel.fullName())
|
||||
});
|
||||
if (response !== loc.yes) { return; }
|
||||
try {
|
||||
await this.databaseModel.delete();
|
||||
vscode.window.showInformationMessage(loc.serviceDeleted(this.databaseModel.fullName()));
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(loc.serviceDeletionFailed(this.databaseModel.fullName(), error));
|
||||
}
|
||||
});
|
||||
|
||||
// Open in Azure portal
|
||||
const openInAzurePortalButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
|
||||
label: loc.openInAzurePortal,
|
||||
iconPath: IconPathHelper.openInTab
|
||||
}).component();
|
||||
|
||||
openInAzurePortalButton.onDidClick(async () => {
|
||||
const r = this.controllerModel.registration('postgresInstances', this.databaseModel.namespace(), this.databaseModel.name());
|
||||
if (r === undefined) {
|
||||
vscode.window.showErrorMessage(loc.couldNotFindAzureResource(this.databaseModel.fullName()));
|
||||
} else {
|
||||
vscode.env.openExternal(vscode.Uri.parse(
|
||||
`https://portal.azure.com/#resource/subscriptions/${r.subscriptionId}/resourceGroups/${r.resourceGroupName}/providers/Microsoft.AzureData/postgresInstances/${r.instanceName}`));
|
||||
}
|
||||
});
|
||||
|
||||
// TODO implement click
|
||||
const feedbackButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
|
||||
label: loc.feedback,
|
||||
iconPath: IconPathHelper.heart
|
||||
}).component();
|
||||
|
||||
return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([
|
||||
{ component: newDatabaseButton },
|
||||
{ component: resetPasswordButton },
|
||||
{ component: deleteButton, toolbarSeparatorAfter: true },
|
||||
{ component: openInAzurePortalButton },
|
||||
{ component: feedbackButton }
|
||||
]).component();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as azdata from 'azdata';
|
||||
import * as loc from '../../../localizedConstants';
|
||||
import { IconPathHelper, cssStyles } from '../../../constants';
|
||||
import { PostgresDashboardPage } from './postgresDashboardPage';
|
||||
import { KeyValueContainer, KeyValue, InputKeyValue, LinkKeyValue, TextKeyValue } from '../../components/keyValueContainer';
|
||||
|
||||
|
||||
export class PostgresPropertiesPage extends PostgresDashboardPage {
|
||||
protected get title(): string {
|
||||
return loc.properties;
|
||||
}
|
||||
|
||||
protected get id(): string {
|
||||
return 'postgres-properties';
|
||||
}
|
||||
|
||||
protected get icon(): { dark: string; light: string; } {
|
||||
return IconPathHelper.properties;
|
||||
}
|
||||
|
||||
protected get container(): azdata.Component {
|
||||
const root = this.modelView.modelBuilder.divContainer().component();
|
||||
const content = this.modelView.modelBuilder.divContainer().component();
|
||||
root.addItem(content, { CSSStyles: { 'margin': '20px' } });
|
||||
|
||||
content.addItem(this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
value: loc.properties,
|
||||
CSSStyles: { ...cssStyles.title, 'margin-bottom': '25px' }
|
||||
}).component());
|
||||
|
||||
const endpoint: { ip?: string, port?: number } = this.databaseModel.endpoint();
|
||||
const connectionString = `postgresql://postgres:${this.databaseModel.password()}@${endpoint.ip}:${endpoint.port}`;
|
||||
const registration = this.controllerModel.registration('postgresInstances', this.databaseModel.namespace(), this.databaseModel.name());
|
||||
|
||||
const pairs: KeyValue[] = [
|
||||
new InputKeyValue(loc.coordinatorEndpoint, connectionString),
|
||||
new InputKeyValue(loc.postgresAdminUsername, 'postgres'),
|
||||
new TextKeyValue(loc.status, this.databaseModel.service().status?.state ?? 'Unknown'),
|
||||
new LinkKeyValue(loc.dataController, this.controllerModel.namespace(), _ => vscode.window.showInformationMessage('goto data controller')),
|
||||
new LinkKeyValue(loc.nodeConfiguration, this.databaseModel.configuration(), _ => vscode.window.showInformationMessage('goto configuration')),
|
||||
new TextKeyValue(loc.postgresVersion, this.databaseModel.service().spec.engine.version?.toString() ?? ''),
|
||||
new TextKeyValue(loc.resourceGroup, registration?.resourceGroupName ?? ''),
|
||||
new TextKeyValue(loc.subscriptionId, registration?.subscriptionId ?? '')
|
||||
];
|
||||
|
||||
const keyValueContainer = new KeyValueContainer(this.modelView.modelBuilder, pairs);
|
||||
content.addItem(keyValueContainer.container);
|
||||
return root;
|
||||
}
|
||||
|
||||
protected get toolbarContainer(): azdata.ToolbarContainer {
|
||||
return this.modelView.modelBuilder.toolbarContainer().component();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user