diff --git a/extensions/arc/src/common/utils.ts b/extensions/arc/src/common/utils.ts index fb988a50cb..4230c2f17b 100644 --- a/extensions/arc/src/common/utils.ts +++ b/extensions/arc/src/common/utils.ts @@ -3,6 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as vscode from 'vscode'; +import * as azurecore from '../../../azurecore/src/azurecore'; import * as loc from '../localizedConstants'; export enum ResourceType { @@ -35,3 +37,15 @@ export function parseEndpoint(endpoint?: string): { ip: string, port: string } { port: endpoint.substr(separatorIndex + 1) }; } + +let azurecoreApi: azurecore.IExtension; + +export async function getAzurecoreApi(): Promise { + if (!azurecoreApi) { + azurecoreApi = await vscode.extensions.getExtension(azurecore.extension.name)?.activate(); + if (!azurecoreApi) { + throw new Error('Unable to retrieve azurecore API'); + } + } + return azurecoreApi; +} diff --git a/extensions/arc/src/controller/generated/v1/api/sqlInstanceRouterApi.ts b/extensions/arc/src/controller/generated/v1/api/sqlInstanceRouterApi.ts index 1942160e80..b1adfabefe 100644 --- a/extensions/arc/src/controller/generated/v1/api/sqlInstanceRouterApi.ts +++ b/extensions/arc/src/controller/generated/v1/api/sqlInstanceRouterApi.ts @@ -17,6 +17,7 @@ import http = require('http'); import { CreateSqlInstanceMessage } from '../model/createSqlInstanceMessage'; import { Authentication, HttpBasicAuth, HttpBearerAuth, Interceptor, ObjectSerializer, VoidAuth } from '../model/models'; import { HttpError } from './apis'; +import { HybridSqlNsNameGetResponse } from '../model/hybridSqlNsNameGetResponse'; @@ -307,7 +308,7 @@ export class SqlInstanceRouterApi { * @param ns * @param name */ - public async apiV1HybridSqlNsNameGet (ns: string, name: string, options: {headers: {[name: string]: string}} = {headers: {}}) : Promise<{ response: http.IncomingMessage; body?: any; }> { + public async apiV1HybridSqlNsNameGet (ns: string, name: string, options: {headers: {[name: string]: string}} = {headers: {}}) : Promise<{ response: http.IncomingMessage; body: HybridSqlNsNameGetResponse; }> { const localVarPath = this.basePath + '/api/v1/hybrid/sql/{ns}/{name}' .replace('{' + 'ns' + '}', encodeURIComponent(String(ns))) .replace('{' + 'name' + '}', encodeURIComponent(String(name))); @@ -360,11 +361,12 @@ export class SqlInstanceRouterApi { localVarRequestOptions.form = localVarFormParams; } } - return new Promise<{ response: http.IncomingMessage; body?: any; }>((resolve, reject) => { + return new Promise<{ response: http.IncomingMessage; body: HybridSqlNsNameGetResponse; }>((resolve, reject) => { localVarRequest(localVarRequestOptions, (error, response, body) => { if (error) { reject(error); } else { + body = ObjectSerializer.deserialize(body, "HybridSqlNsNameGetResponse"); if (response.statusCode && response.statusCode >= 200 && response.statusCode <= 299) { resolve({ response: response, body: body }); } else { diff --git a/extensions/arc/src/controller/generated/v1/model/hybridSqlNsNameGetResponse.ts b/extensions/arc/src/controller/generated/v1/model/hybridSqlNsNameGetResponse.ts new file mode 100644 index 0000000000..c8b5970580 --- /dev/null +++ b/extensions/arc/src/controller/generated/v1/model/hybridSqlNsNameGetResponse.ts @@ -0,0 +1,22 @@ +/** + * SQL Server Big Data Cluster API + * OpenAPI specification for **SQL Server Big Data Cluster**. + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export class HybridSqlNsNameGetResponse { + 'name'?: string; + 'status'?: string; + 'cluster_endpoint'?: string; + 'external_endpoint'?: string; + 'service_name'?: string; + 'vcores'?: string; +} + diff --git a/extensions/arc/src/extension.ts b/extensions/arc/src/extension.ts index 79b255095f..58b4400ad9 100644 --- a/extensions/arc/src/extension.ts +++ b/extensions/arc/src/extension.ts @@ -56,11 +56,12 @@ export async function activate(context: vscode.ExtensionContext): Promise groupId: undefined, options: connection.options }; + const instanceNamespace = ''; const instanceName = ''; try { const controllerModel = new ControllerModel(controllerUrl, auth); - const miaaModel = new MiaaModel(connectionProfile, instanceName); + const miaaModel = new MiaaModel(connectionProfile, controllerUrl, auth, instanceNamespace, instanceName); const miaaDashboard = new MiaaDashboard(controllerModel, miaaModel); await Promise.all([ diff --git a/extensions/arc/src/localizedConstants.ts b/extensions/arc/src/localizedConstants.ts index c4603f90e9..9d976ee140 100644 --- a/extensions/arc/src/localizedConstants.ts +++ b/extensions/arc/src/localizedConstants.ts @@ -32,7 +32,6 @@ export const resetPassword = localize('arc.resetPassword', "Reset Password"); export const openInAzurePortal = localize('arc.openInAzurePortal', "Open in Azure Portal"); export const resourceGroup = localize('arc.resourceGroup', "Resource Group"); export const region = localize('arc.region', "Region"); -export const subscription = localize('arc.subscription', "Subscription"); export const subscriptionId = localize('arc.subscriptionId', "Subscription ID"); export const state = localize('arc.state', "State"); export const connectionMode = localize('arc.connectionMode', "Connection Mode"); @@ -41,12 +40,14 @@ export const host = localize('arc.host', "Host"); export const name = localize('arc.name', "Name"); export const type = localize('arc.type', "Type"); export const status = localize('arc.status', "Status"); +export const miaaAdmin = localize('arc.miaaAdmin', "Managed instance admin"); export const dataController = localize('arc.dataController', "Data controller"); export const kibanaDashboard = localize('arc.kibanaDashboard', "Kibana Dashboard"); export const grafanaDashboard = localize('arc.grafanaDashboard', "Grafana Dashboard"); export const kibanaDashboardDescription = localize('arc.kibanaDashboardDescription', "Dashboard for viewing logs"); export const grafanaDashboardDescription = localize('arc.grafanaDashboardDescription', "Dashboard for viewing metrics"); export const serviceEndpoints = localize('arc.serviceEndpoints', "Service endpoints"); +export const databases = localize('arc.databases', "Databases"); export const endpoint = localize('arc.endpoint', "Endpoint"); export const description = localize('arc.description', "Description"); export const yes = localize('arc.yes', "Yes"); diff --git a/extensions/arc/src/models/controllerModel.ts b/extensions/arc/src/models/controllerModel.ts index 119c9441d9..a5854489cf 100644 --- a/extensions/arc/src/models/controllerModel.ts +++ b/extensions/arc/src/models/controllerModel.ts @@ -61,19 +61,19 @@ export class ControllerModel { ]); } - public endpoints(): EndpointModel[] { + public get endpoints(): EndpointModel[] { return this._endpoints; } - public endpoint(name: string): EndpointModel | undefined { + public getEndpoint(name: string): EndpointModel | undefined { return this._endpoints.find(e => e.name === name); } - public namespace(): string { + public get namespace(): string { return this._namespace; } - public registrations(): Registration[] { + public get registrations(): Registration[] { return this._registrations; } diff --git a/extensions/arc/src/models/miaaModel.ts b/extensions/arc/src/models/miaaModel.ts index 0e41d9d390..2a43cbbbf1 100644 --- a/extensions/arc/src/models/miaaModel.ts +++ b/extensions/arc/src/models/miaaModel.ts @@ -5,24 +5,72 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; +import { SqlInstanceRouterApi } from '../controller/generated/v1/api/sqlInstanceRouterApi'; +import { HybridSqlNsNameGetResponse } from '../controller/generated/v1/model/hybridSqlNsNameGetResponse'; +import { Authentication } from '../controller/generated/v1/api'; + +export type DatabaseModel = { name: string, status: string }; export class MiaaModel { + + private _sqlInstanceRouter: SqlInstanceRouterApi; + private _status: HybridSqlNsNameGetResponse | undefined; + private readonly _onPasswordUpdated = new vscode.EventEmitter(); + private readonly _onStatusUpdated = new vscode.EventEmitter(); + private readonly _onDatabasesUpdated = new vscode.EventEmitter(); public onPasswordUpdated = this._onPasswordUpdated.event; + public onStatusUpdated = this._onStatusUpdated.event; + public onDatabasesUpdated = this._onDatabasesUpdated.event; public passwordLastUpdated?: Date; - constructor(public connectionProfile: azdata.IConnectionProfile, private _name: string) { + constructor(public connectionProfile: azdata.IConnectionProfile, controllerUrl: string, auth: Authentication, private _namespace: string, private _name: string) { + this._sqlInstanceRouter = new SqlInstanceRouterApi(controllerUrl); + this._sqlInstanceRouter.setDefaultAuthentication(auth); } - /** Returns the service's name */ + /** + * The name of this instance + */ public get name(): string { return this._name; } + /** + * The namespace of this instance + */ + public get namespace(): string { + return this._namespace; + } + + /** + * The status of this instance + */ + public get status(): string { + return this._status?.status || ''; + } + + /** + * The cluster endpoint of this instance + */ + public get clusterEndpoint(): string { + return this._status?.cluster_endpoint || ''; + } + + public get databases(): DatabaseModel[] { + return [ + { name: 'contosoMI54', status: 'online' }, + { name: 'contosoMI56', status: 'online' }, + { name: 'contosoMI58', status: 'online' }, + ]; + } /** Refreshes the model */ - public async refresh() { - await Promise.all([ - ]); + public async refresh(): Promise { + this._sqlInstanceRouter.apiV1HybridSqlNsNameGet(this._namespace, this._name).then(response => { + this._status = response.body; + this._onStatusUpdated.fire(this._status); + this._onDatabasesUpdated.fire(this.databases); + }); } } diff --git a/extensions/arc/src/models/postgresModel.ts b/extensions/arc/src/models/postgresModel.ts index 50e92003d5..b18de9dd88 100644 --- a/extensions/arc/src/models/postgresModel.ts +++ b/extensions/arc/src/models/postgresModel.ts @@ -35,32 +35,32 @@ export class PostgresModel { } /** Returns the service's Kubernetes namespace */ - public namespace(): string { + public get namespace(): string { return this._namespace; } /** Returns the service's name */ - public name(): string { + public get name(): string { return this._name; } /** Returns the service's fully qualified name in the format namespace.name */ - public fullName(): string { + public get fullName(): string { return `${this._namespace}.${this._name}`; } /** Returns the service's spec */ - public service(): DuskyObjectModelsDatabaseService | undefined { + public get service(): DuskyObjectModelsDatabaseService | undefined { return this._service; } /** Returns the service's password */ - public password(): string | undefined { + public get password(): string | undefined { return this._password; } /** Returns the service's pods */ - public pods(): V1Pod[] | undefined { + public get pods(): V1Pod[] | undefined { return this._pods; } @@ -95,7 +95,7 @@ export class PostgresModel { service.status = undefined; // can't update the status func(service); - return await this._databaseRouter.updateDuskyDatabaseService(this.namespace(), this.name(), service).then(r => { + return await this._databaseRouter.updateDuskyDatabaseService(this.namespace, this.name, service).then(r => { this._service = r.body; return this._service; }); @@ -108,14 +108,14 @@ export class PostgresModel { /** Creates a SQL database in the service */ public async createDatabase(db: DuskyObjectModelsDatabase): Promise { - return await (await this._databaseRouter.createDuskyDatabase(this.namespace(), this.name(), db)).body; + return await (await this._databaseRouter.createDuskyDatabase(this.namespace, this.name, db)).body; } /** * Returns the IP address and port of the service, preferring external IP over * internal IP. If either field is not available it will be set to undefined. */ - public endpoint(): { ip?: string, port?: number } { + public get endpoint(): { ip?: string, port?: number } { const externalIp = this._service?.status?.externalIP; const internalIp = this._service?.status?.internalIP; const externalPort = this._service?.status?.externalPort; @@ -127,7 +127,7 @@ export class PostgresModel { } /** Returns the service's configuration e.g. '3 nodes, 1.5 vCores, 1GiB RAM, 2GiB storage per node' */ - public configuration(): string { + public get configuration(): string { // TODO: Resource requests and limits can be configured per role. Figure out how // to display that in the UI. For now, only show the default configuration. @@ -136,7 +136,7 @@ export class PostgresModel { const cpuRequest = this._service?.spec?.scheduling?._default?.resources?.requests?.['cpu']; const ramRequest = this._service?.spec?.scheduling?._default?.resources?.requests?.['memory']; const storage = this._service?.spec?.storage?.volumeSize; - const nodes = this.pods()?.length; + const nodes = this.pods?.length; let configuration: string[] = []; diff --git a/extensions/arc/src/ui/dashboards/controller/controllerDashboardOverviewPage.ts b/extensions/arc/src/ui/dashboards/controller/controllerDashboardOverviewPage.ts index 0ff753d3e3..22588c5113 100644 --- a/extensions/arc/src/ui/dashboards/controller/controllerDashboardOverviewPage.ts +++ b/extensions/arc/src/ui/dashboards/controller/controllerDashboardOverviewPage.ts @@ -9,7 +9,7 @@ import * as loc from '../../../localizedConstants'; import { DashboardPage } from '../../components/dashboardPage'; import { IconPathHelper, cssStyles } from '../../../constants'; import { ControllerModel } from '../../../models/controllerModel'; -import { resourceTypeToDisplayName, ResourceType } from '../../../common/utils'; +import { resourceTypeToDisplayName, ResourceType, getAzurecoreApi } from '../../../common/utils'; import { RegistrationResponse } from '../../../controller/generated/v1/model/registrationResponse'; export class ControllerDashboardOverviewPage extends DashboardPage { @@ -21,7 +21,7 @@ export class ControllerDashboardOverviewPage extends DashboardPage { super(modelView); this._controllerModel.onRegistrationsUpdated((_: RegistrationResponse[]) => { this.eventuallyRunOnInitialized(() => { - this.handleRegistrationsUpdated(); + this.handleRegistrationsUpdated().catch(e => console.log(e)); }); }); this.refresh().catch(e => { @@ -141,7 +141,7 @@ export class ControllerDashboardOverviewPage extends DashboardPage { ).component(); } - private handleRegistrationsUpdated(): void { + private async handleRegistrationsUpdated(): Promise { const reg = this._controllerModel.controllerRegistration; if (reg) { this._propertiesContainer.propertyItems = [ @@ -155,7 +155,7 @@ export class ControllerDashboardOverviewPage extends DashboardPage { }, { displayName: loc.region, - value: reg.location || '-' + value: (await getAzurecoreApi()).getRegionDisplayName(reg.location) || '-' }, { displayName: loc.subscriptionId, @@ -180,7 +180,7 @@ export class ControllerDashboardOverviewPage extends DashboardPage { ]; } - this._arcResourcesTable.data = this._controllerModel.registrations() + this._arcResourcesTable.data = this._controllerModel.registrations .filter(r => r.instanceType !== ResourceType.dataControllers) .map(r => [r.instanceName, resourceTypeToDisplayName(r.instanceType), r.vCores]); } diff --git a/extensions/arc/src/ui/dashboards/miaa/miaaDashboard.ts b/extensions/arc/src/ui/dashboards/miaa/miaaDashboard.ts index 6b7b87d5cc..9a1a024224 100644 --- a/extensions/arc/src/ui/dashboards/miaa/miaaDashboard.ts +++ b/extensions/arc/src/ui/dashboards/miaa/miaaDashboard.ts @@ -18,7 +18,7 @@ export class MiaaDashboard extends Dashboard { } protected async registerTabs(modelView: azdata.ModelView): Promise<(azdata.DashboardTab | azdata.DashboardTabGroup)[]> { - const overviewPage = new MiaaDashboardOverviewPage(modelView, this._controllerModel); + const overviewPage = new MiaaDashboardOverviewPage(modelView, this._controllerModel, this._miaaModel); const connectionStringsPage = new MiaaConnectionStringsPage(modelView, this._controllerModel, this._miaaModel); return [ overviewPage.tab, diff --git a/extensions/arc/src/ui/dashboards/miaa/miaaDashboardOverviewPage.ts b/extensions/arc/src/ui/dashboards/miaa/miaaDashboardOverviewPage.ts index 73e159d257..b6995af9d3 100644 --- a/extensions/arc/src/ui/dashboards/miaa/miaaDashboardOverviewPage.ts +++ b/extensions/arc/src/ui/dashboards/miaa/miaaDashboardOverviewPage.ts @@ -6,16 +6,48 @@ import * as azdata from 'azdata'; import * as loc from '../../../localizedConstants'; import { DashboardPage } from '../../components/dashboardPage'; -import { IconPathHelper } from '../../../constants'; -import { ControllerModel } from '../../../models/controllerModel'; -import { resourceTypeToDisplayName } from '../../../common/utils'; +import { IconPathHelper, cssStyles } from '../../../constants'; +import { ControllerModel, Registration } from '../../../models/controllerModel'; +import { ResourceType, getAzurecoreApi } from '../../../common/utils'; +import { MiaaModel, DatabaseModel } from '../../../models/miaaModel'; +import { HybridSqlNsNameGetResponse } from '../../../controller/generated/v1/model/hybridSqlNsNameGetResponse'; +import { EndpointModel } from '../../../controller/generated/v1/api'; export class MiaaDashboardOverviewPage extends DashboardPage { - private _arcResourcesTable!: azdata.DeclarativeTableComponent; + private _propertiesLoading!: azdata.LoadingComponent; + private _kibanaLoading!: azdata.LoadingComponent; + private _grafanaLoading!: azdata.LoadingComponent; + private _databasesTableLoading!: azdata.LoadingComponent; - constructor(modelView: azdata.ModelView, private _controllerModel: ControllerModel) { + private _propertiesContainer!: azdata.PropertiesContainerComponent; + private _kibanaLink!: azdata.HyperlinkComponent; + private _grafanaLink!: azdata.HyperlinkComponent; + private _databasesTable!: azdata.DeclarativeTableComponent; + + private _instanceProperties = { + resourceGroup: '-', + status: '-', + dataController: '-', + region: '-', + subscriptionId: '-', + miaaAdmin: '-', + host: '-', + computeAndStorage: '-' + }; + + constructor(modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _miaaModel: MiaaModel) { super(modelView); + this._instanceProperties.miaaAdmin = this._miaaModel.connectionProfile.userName; + this._controllerModel.onRegistrationsUpdated((_: Registration[]) => { + this.eventuallyRunOnInitialized(() => { + this.handleRegistrationsUpdated().catch(e => console.log(e)); + }); + }); + this._controllerModel.onEndpointsUpdated(endpoints => this.eventuallyRunOnInitialized(() => this.handleEndpointsUpdated(endpoints))); + this._miaaModel.onStatusUpdated(status => this.eventuallyRunOnInitialized(() => this.handleMiaaStatusUpdated(status))); + this._miaaModel.onDatabasesUpdated(databases => this.eventuallyRunOnInitialized(() => this.handleDatabasesUpdated(databases))); + this.refresh().catch(e => { console.log(e); }); @@ -34,8 +66,7 @@ export class MiaaDashboardOverviewPage extends DashboardPage { } protected async refresh(): Promise { - await this._controllerModel.refresh(); - this.eventuallyRunOnInitialized(() => this._arcResourcesTable.data = this._controllerModel.registrations().map(r => [r.instanceName, resourceTypeToDisplayName(r.instanceType), r.vCores])); + await Promise.all([this._controllerModel.refresh(), this._miaaModel.refresh()]); } public get container(): azdata.Component { @@ -45,76 +76,90 @@ export class MiaaDashboardOverviewPage extends DashboardPage { .withProperties({ CSSStyles: { 'margin': '18px' } }) .component(); - const propertiesContainer = this.modelView.modelBuilder.propertiesContainer().withProperties({ - propertyItems: [ - { - displayName: loc.resourceGroup, - value: 'contosoRG123' - }, - { - displayName: loc.region, - value: 'West US' - }, - { - displayName: loc.subscription, - value: 'contososub5678' - }, - { - displayName: loc.subscriptionId, - value: '88abe223-c630-4f2c-8782-00bb5be874f6' - }, - { - displayName: loc.state, - value: 'Connected' - }, - { - displayName: loc.host, - value: 'plainscluster.sqlarcdm.database.windows.net' - } - ] - }).component(); + // Properties + this._propertiesContainer = this.modelView.modelBuilder.propertiesContainer().component(); + this._propertiesLoading = this.modelView.modelBuilder.loadingComponent().withItem(this._propertiesContainer).component(); + rootContainer.addItem(this._propertiesLoading, { CSSStyles: cssStyles.text }); - rootContainer.addItem(propertiesContainer); + // Service endpoints + const titleCSS = { ...cssStyles.title, 'margin-block-start': '2em', 'margin-block-end': '0' }; + rootContainer.addItem(this.modelView.modelBuilder.text().withProperties({ value: loc.serviceEndpoints, CSSStyles: titleCSS }).component()); - const arcResourcesTitle = this.modelView.modelBuilder.text() - .withProperties({ value: loc.arcResources }) - .component(); + this._kibanaLink = this.modelView.modelBuilder.hyperlink().component(); + this._grafanaLink = this.modelView.modelBuilder.hyperlink().component(); + this._kibanaLoading = this.modelView.modelBuilder.loadingComponent().withItem(this._kibanaLink).component(); + this._grafanaLoading = this.modelView.modelBuilder.loadingComponent().withItem(this._grafanaLink).component(); - rootContainer.addItem(arcResourcesTitle, { - CSSStyles: { - 'font-size': '14px' - } - }); - - this._arcResourcesTable = this.modelView.modelBuilder.declarativeTable().withProperties({ - data: [], + const endpointsTable = this.modelView.modelBuilder.declarativeTable().withProperties({ + width: '100%', columns: [ { displayName: loc.name, valueType: azdata.DeclarativeDataType.string, - width: '33%', - isReadOnly: true - }, { - displayName: loc.type, + 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, - width: '33%', - isReadOnly: true - }, { - displayName: loc.computeAndStorage, - valueType: azdata.DeclarativeDataType.string, - width: '34%', - isReadOnly: true + isReadOnly: true, + width: '30%', + headerCssStyles: cssStyles.tableHeader, + rowCssStyles: cssStyles.tableRow } ], - width: '100%', - ariaLabel: loc.arcResources + data: [ + [loc.kibanaDashboard, this._kibanaLoading, loc.kibanaDashboardDescription], + [loc.grafanaDashboard, this._grafanaLoading, loc.grafanaDashboardDescription]] }).component(); - const arcResourcesTableContainer = this.modelView.modelBuilder.divContainer() - .withItems([this._arcResourcesTable]) - .component(); + rootContainer.addItem(endpointsTable); + + // Databases + rootContainer.addItem(this.modelView.modelBuilder.text().withProperties({ value: loc.databases, CSSStyles: titleCSS }).component()); + this._databasesTable = this.modelView.modelBuilder.declarativeTable().withProperties({ + width: '100%', + columns: [ + { + displayName: loc.name, + valueType: azdata.DeclarativeDataType.string, + isReadOnly: true, + width: '80%', + headerCssStyles: cssStyles.tableHeader, + rowCssStyles: cssStyles.tableRow + }, + { + displayName: loc.status, + valueType: azdata.DeclarativeDataType.string, + isReadOnly: true, + width: '20%', + headerCssStyles: cssStyles.tableHeader, + rowCssStyles: cssStyles.tableRow + } + ], + data: [] + }).component(); + + this._databasesTableLoading = this.modelView.modelBuilder.loadingComponent().withItem(this._databasesTable).component(); + this._databasesTableLoading.loading = false; + rootContainer.addItem(this._databasesTableLoading, { CSSStyles: { 'margin-bottom': '20px' } }); - rootContainer.addItem(arcResourcesTableContainer); this.initialized = true; return rootContainer; } @@ -151,4 +196,80 @@ export class MiaaDashboardOverviewPage extends DashboardPage { ).component(); } + private async handleRegistrationsUpdated(): Promise { + const reg = this._controllerModel.getRegistration(ResourceType.sqlManagedInstances, this._miaaModel.namespace, this._miaaModel.name); + if (reg) { + this._instanceProperties.resourceGroup = reg.resourceGroupName || '-'; + this._instanceProperties.dataController = this._controllerModel.controllerRegistration?.instanceName || '-'; + this._instanceProperties.region = (await getAzurecoreApi()).getRegionDisplayName(reg.location); + this._instanceProperties.subscriptionId = reg.subscriptionId || '-'; + this._instanceProperties.computeAndStorage = reg.vCores || '-'; + this._instanceProperties.host = reg.externalEndpoint || '-'; + this.refreshDisplayedProperties(); + } + } + + private async handleMiaaStatusUpdated(status: HybridSqlNsNameGetResponse): Promise { + this._instanceProperties.status = status.status || '-'; + this.refreshDisplayedProperties(); + } + + private handleEndpointsUpdated(endpoints: EndpointModel[]): void { + const kibanaQuery = `kubernetes_namespace:"${this._miaaModel.namespace}" and instance_name :"${this._miaaModel.name}"`; + const kibanaUrl = `${endpoints.find(e => e.name === 'logsui')?.endpoint}/app/kibana#/discover?_a=(query:(language:kuery,query:'${kibanaQuery}'))`; + this._kibanaLink.label = kibanaUrl; + this._kibanaLink.url = kibanaUrl; + + const grafanaUrl = `${endpoints.find(e => e.name === 'metricsui')?.endpoint}/d/wZx3OUdmz/azure-sql-db-managed-instance-metrics?var-hostname=${this._miaaModel.name}-0`; + this._grafanaLink.label = grafanaUrl; + this._grafanaLink.url = grafanaUrl; + + this._kibanaLoading!.loading = false; + this._grafanaLoading!.loading = false; + } + + private handleDatabasesUpdated(databases: DatabaseModel[]): void { + this._databasesTable.data = databases.map(d => [d.name, d.status]); + this._databasesTableLoading.loading = false; + } + + private refreshDisplayedProperties(): void { + this._propertiesContainer.propertyItems = [ + { + displayName: loc.resourceGroup, + value: this._instanceProperties.resourceGroup + }, + { + displayName: loc.status, + value: this._instanceProperties.status + }, + { + displayName: loc.dataController, + value: this._instanceProperties.dataController + }, + { + displayName: loc.region, + value: this._instanceProperties.region + }, + { + displayName: loc.subscriptionId, + value: this._instanceProperties.subscriptionId + }, + { + displayName: loc.miaaAdmin, + value: this._instanceProperties.miaaAdmin + }, + { + displayName: loc.host, + value: this._instanceProperties.host + }, + { + displayName: loc.computeAndStorage, + value: this._instanceProperties.computeAndStorage + } + ]; + + this._propertiesLoading.loading = false; + } + } diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts index 0bbf26f4a7..05fa9f0b00 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts @@ -85,7 +85,7 @@ export class PostgresConnectionStringsPage extends DashboardPage { } private refresh() { - const endpoint: { ip?: string, port?: number } = this._postgresModel.endpoint(); + const endpoint: { ip?: string, port?: number } = this._postgresModel.endpoint; this.keyValueContainer?.refresh([ new InputKeyValue('ADO.NET', `Server=${endpoint.ip};Database=postgres;Port=${endpoint.port};User Id=postgres;Password={your_password_here};Ssl Mode=Require;`), diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresDiagnoseAndSolveProblemsPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresDiagnoseAndSolveProblemsPage.ts index e7a90f7f6f..120b16f5f0 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresDiagnoseAndSolveProblemsPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresDiagnoseAndSolveProblemsPage.ts @@ -49,8 +49,8 @@ export class PostgresDiagnoseAndSolveProblemsPage extends DashboardPage { }).component(); troubleshootButton.onDidClick(() => { - process.env['POSTGRES_SERVER_NAMESPACE'] = this._postgresModel.namespace(); - process.env['POSTGRES_SERVER_NAME'] = this._postgresModel.name(); + process.env['POSTGRES_SERVER_NAMESPACE'] = this._postgresModel.namespace; + process.env['POSTGRES_SERVER_NAME'] = this._postgresModel.name; vscode.commands.executeCommand('bookTreeView.openBook', this._context.asAbsolutePath('notebooks/arc'), true, 'postgres/tsg100-troubleshoot-postgres'); }); diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts index 719e8b8552..be587e1fc5 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts @@ -204,9 +204,9 @@ export class PostgresOverviewPage extends DashboardPage { s.arc = s.arc ?? new DuskyObjectModelsDatabaseServiceArcPayload(); s.arc.servicePassword = password; }); - vscode.window.showInformationMessage(loc.passwordReset(this._postgresModel.fullName())); + vscode.window.showInformationMessage(loc.passwordReset(this._postgresModel.fullName)); } catch (error) { - vscode.window.showErrorMessage(loc.passwordResetFailed(this._postgresModel.fullName(), error)); + vscode.window.showErrorMessage(loc.passwordResetFailed(this._postgresModel.fullName, error)); } finally { resetPasswordButton.enabled = true; } @@ -222,13 +222,13 @@ export class PostgresOverviewPage extends DashboardPage { deleteButton.enabled = false; try { const response = await vscode.window.showQuickPick([loc.yes, loc.no], { - placeHolder: loc.deleteServicePrompt(this._postgresModel.fullName()) + placeHolder: loc.deleteServicePrompt(this._postgresModel.fullName) }); if (response !== loc.yes) { return; } await this._postgresModel.delete(); - vscode.window.showInformationMessage(loc.serviceDeleted(this._postgresModel.fullName())); + vscode.window.showInformationMessage(loc.serviceDeleted(this._postgresModel.fullName)); } catch (error) { - vscode.window.showErrorMessage(loc.serviceDeletionFailed(this._postgresModel.fullName(), error)); + vscode.window.showErrorMessage(loc.serviceDeletionFailed(this._postgresModel.fullName, error)); } finally { deleteButton.enabled = true; } @@ -267,9 +267,9 @@ export class PostgresOverviewPage extends DashboardPage { }).component(); openInAzurePortalButton.onDidClick(async () => { - const r = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace(), this._postgresModel.name()); + const r = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name); if (!r) { - vscode.window.showErrorMessage(loc.couldNotFindAzureResource(this._postgresModel.fullName())); + vscode.window.showErrorMessage(loc.couldNotFindAzureResource(this._postgresModel.fullName)); } else { vscode.env.openExternal(vscode.Uri.parse( `https://portal.azure.com/#resource/subscriptions/${r.subscriptionId}/resourceGroups/${r.resourceGroupName}/providers/Microsoft.AzureData/${ResourceType.postgresInstances}/${r.instanceName}`)); @@ -286,30 +286,30 @@ export class PostgresOverviewPage extends DashboardPage { } private refreshProperties() { - const registration = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace(), this._postgresModel.name()); - const endpoint: { ip?: string, port?: number } = this._postgresModel.endpoint(); + const registration = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name); + const endpoint: { ip?: string, port?: number } = this._postgresModel.endpoint; this.properties!.propertyItems = [ - { displayName: loc.name, value: this._postgresModel.name() }, - { displayName: loc.coordinatorEndpoint, value: `postgresql://postgres:${this._postgresModel.password()}@${endpoint.ip}:${endpoint.port}` }, - { displayName: loc.status, value: this._postgresModel.service()?.status?.state ?? '' }, + { displayName: loc.name, value: this._postgresModel.name }, + { displayName: loc.coordinatorEndpoint, value: `postgresql://postgres:${this._postgresModel.password}@${endpoint.ip}:${endpoint.port}` }, + { displayName: loc.status, value: this._postgresModel.service?.status?.state ?? '' }, { displayName: loc.postgresAdminUsername, value: 'postgres' }, - { displayName: loc.dataController, value: this._controllerModel?.namespace() ?? '' }, - { displayName: loc.nodeConfiguration, value: this._postgresModel.configuration() }, + { displayName: loc.dataController, value: this._controllerModel?.namespace ?? '' }, + { displayName: loc.nodeConfiguration, value: this._postgresModel.configuration }, { displayName: loc.subscriptionId, value: registration?.subscriptionId ?? '' }, - { displayName: loc.postgresVersion, value: this._postgresModel.service()?.spec?.engine?.version?.toString() ?? '' } + { displayName: loc.postgresVersion, value: this._postgresModel.service?.spec?.engine?.version?.toString() ?? '' } ]; this.propertiesLoading!.loading = false; } private refreshEndpoints() { - const kibanaQuery = `kubernetes_namespace:"${this._postgresModel.namespace()}" and cluster_name:"${this._postgresModel.name()}"`; - const kibanaUrl = `${this._controllerModel.endpoint('logsui')?.endpoint}/app/kibana#/discover?_a=(query:(language:kuery,query:'${kibanaQuery}'))`; + const kibanaQuery = `kubernetes_namespace:"${this._postgresModel.namespace}" and cluster_name:"${this._postgresModel.name}"`; + const kibanaUrl = `${this._controllerModel.getEndpoint('logsui')?.endpoint}/app/kibana#/discover?_a=(query:(language:kuery,query:'${kibanaQuery}'))`; this.kibanaLink!.label = kibanaUrl; this.kibanaLink!.url = kibanaUrl; - const grafanaUrl = `${this._controllerModel.endpoint('metricsui')?.endpoint}/d/postgres-metrics?var-Namespace=${this._postgresModel.namespace()}&var-Name=${this._postgresModel.name()}`; + const grafanaUrl = `${this._controllerModel.getEndpoint('metricsui')?.endpoint}/d/postgres-metrics?var-Namespace=${this._postgresModel.namespace}&var-Name=${this._postgresModel.name}`; this.grafanaLink!.label = grafanaUrl; this.grafanaLink!.url = grafanaUrl; @@ -318,9 +318,9 @@ export class PostgresOverviewPage extends DashboardPage { } private refreshNodes() { - const endpoint: { ip?: string, port?: number } = this._postgresModel.endpoint(); + const endpoint: { ip?: string, port?: number } = this._postgresModel.endpoint; - this.nodesTable!.data = this._postgresModel.pods()?.map((pod: V1Pod) => { + this.nodesTable!.data = this._postgresModel.pods?.map((pod: V1Pod) => { const name = pod.metadata?.name; const role: PodRole | undefined = PostgresModel.getPodRole(pod); const service = pod.metadata?.annotations?.['arcdata.microsoft.com/serviceHost']; diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts index 7b94e38b81..60a79c041b 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts @@ -78,17 +78,17 @@ export class PostgresPropertiesPage extends DashboardPage { } private refresh() { - const endpoint: { ip?: string, port?: number } = this._postgresModel.endpoint(); - const connectionString = `postgresql://postgres:${this._postgresModel.password()}@${endpoint.ip}:${endpoint.port}`; - const registration = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace(), this._postgresModel.name()); + const endpoint: { ip?: string, port?: number } = this._postgresModel.endpoint; + const connectionString = `postgresql://postgres:${this._postgresModel.password}@${endpoint.ip}:${endpoint.port}`; + const registration = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name); this.keyValueContainer?.refresh([ new InputKeyValue(loc.coordinatorEndpoint, connectionString), new InputKeyValue(loc.postgresAdminUsername, 'postgres'), - new TextKeyValue(loc.status, this._postgresModel.service()?.status?.state ?? 'Unknown'), - new LinkKeyValue(loc.dataController, this._controllerModel.namespace() ?? '', _ => vscode.window.showInformationMessage('TODO: Go to data controller')), - new LinkKeyValue(loc.nodeConfiguration, this._postgresModel.configuration(), _ => vscode.window.showInformationMessage('TODO: Go to configuration')), - new TextKeyValue(loc.postgresVersion, this._postgresModel.service()?.spec?.engine?.version?.toString() ?? ''), + new TextKeyValue(loc.status, this._postgresModel.service?.status?.state ?? 'Unknown'), + new LinkKeyValue(loc.dataController, this._controllerModel.namespace ?? '', _ => vscode.window.showInformationMessage('TODO: Go to data controller')), + new LinkKeyValue(loc.nodeConfiguration, this._postgresModel.configuration, _ => vscode.window.showInformationMessage('TODO: Go to configuration')), + new TextKeyValue(loc.postgresVersion, this._postgresModel.service?.spec?.engine?.version?.toString() ?? ''), new TextKeyValue(loc.resourceGroup, registration?.resourceGroupName ?? ''), new TextKeyValue(loc.subscriptionId, registration?.subscriptionId ?? '') ]); diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresSupportRequestPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresSupportRequestPage.ts index e5559b1eb9..7a32b56070 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresSupportRequestPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresSupportRequestPage.ts @@ -51,9 +51,9 @@ export class PostgresSupportRequestPage extends DashboardPage { }).component(); supportRequestButton.onDidClick(() => { - const r = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace(), this._postgresModel.name()); + const r = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name); if (!r) { - vscode.window.showErrorMessage(loc.couldNotFindAzureResource(this._postgresModel.fullName())); + vscode.window.showErrorMessage(loc.couldNotFindAzureResource(this._postgresModel.fullName)); } else { vscode.env.openExternal(vscode.Uri.parse( `https://portal.azure.com/#resource/subscriptions/${r.subscriptionId}/resourceGroups/${r.resourceGroupName}/providers/Microsoft.AzureData/${ResourceType.postgresInstances}/${r.instanceName}/supportrequest`));