From c4a85bbd785487d817c78b3de225ac2459914d3b Mon Sep 17 00:00:00 2001 From: Brian Bergeron Date: Tue, 23 Jun 2020 17:31:16 -0700 Subject: [PATCH] Arc Postgres resource health page (#11065) --- extensions/arc/images/fail.svg | 8 + extensions/arc/images/health.svg | 1 + extensions/arc/images/heart.svg | 3 - extensions/arc/images/success.svg | 10 + extensions/arc/src/common/date.ts | 121 +++++++++++ extensions/arc/src/constants.ts | 20 +- extensions/arc/src/localizedConstants.ts | 11 +- .../postgres/postgresConnectionStringsPage.ts | 11 +- .../dashboards/postgres/postgresDashboard.ts | 3 + .../postgres/postgresOverviewPage.ts | 36 +++- .../postgres/postgresPropertiesPage.ts | 15 +- .../postgres/postgresResourceHealthPage.ts | 192 ++++++++++++++++++ 12 files changed, 408 insertions(+), 23 deletions(-) create mode 100644 extensions/arc/images/fail.svg create mode 100644 extensions/arc/images/health.svg delete mode 100644 extensions/arc/images/heart.svg create mode 100644 extensions/arc/images/success.svg create mode 100644 extensions/arc/src/common/date.ts create mode 100644 extensions/arc/src/ui/dashboards/postgres/postgresResourceHealthPage.ts diff --git a/extensions/arc/images/fail.svg b/extensions/arc/images/fail.svg new file mode 100644 index 0000000000..1a87f5e8da --- /dev/null +++ b/extensions/arc/images/fail.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/extensions/arc/images/health.svg b/extensions/arc/images/health.svg new file mode 100644 index 0000000000..b0361922cb --- /dev/null +++ b/extensions/arc/images/health.svg @@ -0,0 +1 @@ +Icon-general-4 \ No newline at end of file diff --git a/extensions/arc/images/heart.svg b/extensions/arc/images/heart.svg deleted file mode 100644 index 3c60b8d751..0000000000 --- a/extensions/arc/images/heart.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/extensions/arc/images/success.svg b/extensions/arc/images/success.svg new file mode 100644 index 0000000000..0a3a05baf7 --- /dev/null +++ b/extensions/arc/images/success.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/extensions/arc/src/common/date.ts b/extensions/arc/src/common/date.ts new file mode 100644 index 0000000000..45f2cee696 --- /dev/null +++ b/extensions/arc/src/common/date.ts @@ -0,0 +1,121 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +// This file was copied from src/vs/base/common/date.ts +const minute = 60; +const hour = minute * 60; +const day = hour * 24; +const week = day * 7; +const month = day * 30; +const year = day * 365; + +export function fromNow(date: number | Date, appendAgoLabel?: boolean): string { + if (typeof date !== 'number') { + date = date.getTime(); + } + + const seconds = Math.round((new Date().getTime() - date) / 1000); + if (seconds < -30) { + return localize('date.fromNow.in', 'in {0}', fromNow(new Date().getTime() + seconds * 1000, false)); + } + + if (seconds < 30) { + return localize('date.fromNow.now', 'now'); + } + + let value: number; + if (seconds < minute) { + value = seconds; + + if (appendAgoLabel) { + return value === 1 + ? localize('date.fromNow.seconds.singular.ago', '{0} sec ago', value) + : localize('date.fromNow.seconds.plural.ago', '{0} secs ago', value); + } else { + return value === 1 + ? localize('date.fromNow.seconds.singular', '{0} sec', value) + : localize('date.fromNow.seconds.plural', '{0} secs', value); + } + } + + if (seconds < hour) { + value = Math.floor(seconds / minute); + if (appendAgoLabel) { + return value === 1 + ? localize('date.fromNow.minutes.singular.ago', '{0} min ago', value) + : localize('date.fromNow.minutes.plural.ago', '{0} mins ago', value); + } else { + return value === 1 + ? localize('date.fromNow.minutes.singular', '{0} min', value) + : localize('date.fromNow.minutes.plural', '{0} mins', value); + } + } + + if (seconds < day) { + value = Math.floor(seconds / hour); + if (appendAgoLabel) { + return value === 1 + ? localize('date.fromNow.hours.singular.ago', '{0} hr ago', value) + : localize('date.fromNow.hours.plural.ago', '{0} hrs ago', value); + } else { + return value === 1 + ? localize('date.fromNow.hours.singular', '{0} hr', value) + : localize('date.fromNow.hours.plural', '{0} hrs', value); + } + } + + if (seconds < week) { + value = Math.floor(seconds / day); + if (appendAgoLabel) { + return value === 1 + ? localize('date.fromNow.days.singular.ago', '{0} day ago', value) + : localize('date.fromNow.days.plural.ago', '{0} days ago', value); + } else { + return value === 1 + ? localize('date.fromNow.days.singular', '{0} day', value) + : localize('date.fromNow.days.plural', '{0} days', value); + } + } + + if (seconds < month) { + value = Math.floor(seconds / week); + if (appendAgoLabel) { + return value === 1 + ? localize('date.fromNow.weeks.singular.ago', '{0} wk ago', value) + : localize('date.fromNow.weeks.plural.ago', '{0} wks ago', value); + } else { + return value === 1 + ? localize('date.fromNow.weeks.singular', '{0} wk', value) + : localize('date.fromNow.weeks.plural', '{0} wks', value); + } + } + + if (seconds < year) { + value = Math.floor(seconds / month); + if (appendAgoLabel) { + return value === 1 + ? localize('date.fromNow.months.singular.ago', '{0} mo ago', value) + : localize('date.fromNow.months.plural.ago', '{0} mos ago', value); + } else { + return value === 1 + ? localize('date.fromNow.months.singular', '{0} mo', value) + : localize('date.fromNow.months.plural', '{0} mos', value); + } + } + + value = Math.floor(seconds / year); + if (appendAgoLabel) { + return value === 1 + ? localize('date.fromNow.years.singular.ago', '{0} yr ago', value) + : localize('date.fromNow.years.plural.ago', '{0} yrs ago', value); + } else { + return value === 1 + ? localize('date.fromNow.years.singular', '{0} yr', value) + : localize('date.fromNow.years.plural', '{0} yrs', value); + } +} diff --git a/extensions/arc/src/constants.ts b/extensions/arc/src/constants.ts index d65a51e4ab..1b351231c2 100644 --- a/extensions/arc/src/constants.ts +++ b/extensions/arc/src/constants.ts @@ -19,7 +19,6 @@ export class IconPathHelper { public static edit: IconPath; public static delete: IconPath; public static openInTab: IconPath; - public static heart: IconPath; public static copy: IconPath; public static collapseUp: IconPath; public static collapseDown: IconPath; @@ -34,6 +33,9 @@ export class IconPathHelper { public static wrench: IconPath; public static miaa: IconPath; public static controller: IconPath; + public static health: IconPath; + public static success: IconPath; + public static fail: IconPath; public static setExtensionContext(context: vscode.ExtensionContext) { IconPathHelper.context = context; @@ -53,10 +55,6 @@ export class IconPathHelper { light: IconPathHelper.context.asAbsolutePath('images/open-in-tab.svg'), dark: IconPathHelper.context.asAbsolutePath('images/open-in-tab.svg') }; - IconPathHelper.heart = { - light: IconPathHelper.context.asAbsolutePath('images/heart.svg'), - dark: IconPathHelper.context.asAbsolutePath('images/heart.svg') - }; IconPathHelper.copy = { light: IconPathHelper.context.asAbsolutePath('images/copy.svg'), dark: IconPathHelper.context.asAbsolutePath('images/copy.svg') @@ -105,6 +103,18 @@ export class IconPathHelper { light: context.asAbsolutePath('images/data_controller.svg'), dark: context.asAbsolutePath('images/data_controller.svg'), }; + IconPathHelper.health = { + light: context.asAbsolutePath('images/health.svg'), + dark: context.asAbsolutePath('images/health.svg'), + }; + IconPathHelper.success = { + light: context.asAbsolutePath('images/success.svg'), + dark: context.asAbsolutePath('images/success.svg'), + }; + IconPathHelper.fail = { + light: context.asAbsolutePath('images/fail.svg'), + dark: context.asAbsolutePath('images/fail.svg'), + }; } } diff --git a/extensions/arc/src/localizedConstants.ts b/extensions/arc/src/localizedConstants.ts index 7f2373c3d9..4d92fff6e8 100644 --- a/extensions/arc/src/localizedConstants.ts +++ b/extensions/arc/src/localizedConstants.ts @@ -28,6 +28,7 @@ export const backup = localize('arc.backup', "Backup"); export const newSupportRequest = localize('arc.newSupportRequest', "New support request"); export const diagnoseAndSolveProblems = localize('arc.diagnoseAndSolveProblems', "Diagnose and solve problems"); export const supportAndTroubleshooting = localize('arc.supportAndTroubleshooting', "Support + troubleshooting"); +export const resourceHealth = localize('arc.resourceHealth', "Resource health"); export const newInstance = localize('arc.createNew', "New Instance"); export const deleteText = localize('arc.delete', "Delete"); @@ -64,6 +65,9 @@ export const refresh = localize('arc.refresh', "Refresh"); export const troubleshoot = localize('arc.troubleshoot', "Troubleshoot"); export const clickTheNewSupportRequestButton = localize('arc.clickTheNewSupportRequestButton', "Click the new support request button to file a support request in the Azure Portal."); export const running = localize('arc.running', "Running"); +export const pending = localize('arc.pending', "Pending"); +export const failed = localize('arc.failed', "Failed"); +export const unknown = localize('arc.unknown', "Unknown"); export const connected = localize('arc.connected', "Connected"); export const disconnected = localize('arc.disconnected', "Disconnected"); export const loading = localize('arc.loading', "Loading..."); @@ -109,7 +113,10 @@ export const arcResources = localize('arc.arcResources', "Azure Arc Resources"); export const enterANonEmptyPassword = localize('arc.enterANonEmptyPassword', "Enter a non empty password or press escape to exit."); export const thePasswordsDoNotMatch = localize('arc.thePasswordsDoNotMatch', "The passwords do not match. Confirm the password or press escape to exit."); export const passwordReset = localize('arc.passwordReset', "Password reset successfully"); -export const passwordResetFailed = localize('arc.passwordResetFailed', "Failed to reset password"); +export const podOverview = localize('arc.podOverview', "Pod overview"); +export const condition = localize('arc.condition', "Condition"); +export const details = localize('arc.details', "Details"); +export const lastUpdated = localize('arc.lastUpdated', "Last updated"); export function databaseCreated(name: string): string { return localize('arc.databaseCreated', "Database {0} created", name); } export function resourceDeleted(name: string): string { return localize('arc.resourceDeleted', "Resource '{0}' deleted", name); } @@ -126,6 +133,7 @@ export function numVCores(vCores: string | undefined): string { export function couldNotFindRegistration(namespace: string, name: string) { return localize('arc.couldNotFindRegistration', "Could not find controller registration for {0} ({1})", name, namespace); } export function resourceDeletionWarning(namespace: string, name: string): string { return localize('arc.resourceDeletionWarning', "Warning! Deleting a resource is permanent and cannot be undone. To delete the resource '{0}.{1}' type the name '{1}' below to proceed.", namespace, name); } export function invalidResourceDeletionName(name: string): string { return localize('arc.invalidResourceDeletionName', "The value '{0}' does not match the instance name. Try again or press escape to exit", name); } +export function updated(when: string): string { return localize('arc.updated', "Updated {0}", when); } // Errors export function refreshFailed(error: any): string { return localize('arc.refreshFailed', "Refresh failed. {0}", getErrorMessage(error)); } @@ -133,3 +141,4 @@ export function openDashboardFailed(error: any): string { return localize('arc.o export function resourceDeletionFailed(name: string, error: any): string { return localize('arc.resourceDeletionFailed', "Failed to delete resource {0}. {1}", name, getErrorMessage(error)); } export function databaseCreationFailed(name: string, error: any): string { return localize('arc.databaseCreationFailed', "Failed to create database {0}. {1}", name, getErrorMessage(error)); } export function connectToControllerFailed(url: string, error: any): string { return localize('arc.connectToControllerFailed', "Could not connect to controller {0}. {1}", url, getErrorMessage(error)); } +export function passwordResetFailed(error: any): string { return localize('arc.passwordResetFailed', "Failed to reset password. {0}", getErrorMessage(error)); } diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts index 1ca1772a39..9638835a92 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts @@ -12,11 +12,20 @@ import { DashboardPage } from '../../components/dashboardPage'; import { PostgresModel } from '../../../models/postgresModel'; export class PostgresConnectionStringsPage extends DashboardPage { + private disposables: vscode.Disposable[] = []; private keyValueContainer?: KeyValueContainer; constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) { super(modelView); - this._postgresModel.onServiceUpdated(() => this.eventuallyRunOnInitialized(() => this.refresh())); + + modelView.onClosed(() => + this.disposables.forEach(d => { + try { d.dispose(); } + catch { } + })); + + this.disposables.push(this._postgresModel.onServiceUpdated( + () => this.eventuallyRunOnInitialized(() => this.refresh()))); } protected get title(): string { diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts b/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts index b3753a485b..738aca22cc 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts @@ -14,6 +14,7 @@ import { PostgresPropertiesPage } from './postgresPropertiesPage'; import { Dashboard } from '../../components/dashboard'; import { PostgresDiagnoseAndSolveProblemsPage } from './postgresDiagnoseAndSolveProblemsPage'; import { PostgresSupportRequestPage } from './postgresSupportRequestPage'; +import { PostgresResourceHealthPage } from './postgresResourceHealthPage'; export class PostgresDashboard extends Dashboard { constructor(private _context: vscode.ExtensionContext, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) { @@ -24,6 +25,7 @@ export class PostgresDashboard extends Dashboard { const overviewPage = new PostgresOverviewPage(modelView, this._controllerModel, this._postgresModel); const connectionStringsPage = new PostgresConnectionStringsPage(modelView, this._postgresModel); const propertiesPage = new PostgresPropertiesPage(modelView, this._controllerModel, this._postgresModel); + const resourceHealthPage = new PostgresResourceHealthPage(modelView, this._postgresModel); const diagnoseAndSolveProblemsPage = new PostgresDiagnoseAndSolveProblemsPage(modelView, this._context, this._postgresModel); const supportRequestPage = new PostgresSupportRequestPage(modelView, this._controllerModel, this._postgresModel); @@ -39,6 +41,7 @@ export class PostgresDashboard extends Dashboard { { title: loc.supportAndTroubleshooting, tabs: [ + resourceHealthPage.tab, diagnoseAndSolveProblemsPage.tab, supportRequestPage.tab ] diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts index 809bc5675c..b6cdacce34 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts @@ -14,6 +14,8 @@ import { PostgresModel, PodRole } from '../../../models/postgresModel'; import { promptForResourceDeletion, promptAndConfirmPassword } from '../../../common/utils'; export class PostgresOverviewPage extends DashboardPage { + private disposables: vscode.Disposable[] = []; + private propertiesLoading?: azdata.LoadingComponent; private kibanaLoading?: azdata.LoadingComponent; private grafanaLoading?: azdata.LoadingComponent; @@ -26,18 +28,30 @@ export class PostgresOverviewPage extends DashboardPage { constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) { super(modelView); - this._controllerModel.onEndpointsUpdated(() => this.eventuallyRunOnInitialized(() => this.refreshEndpoints())); - this._controllerModel.onRegistrationsUpdated(() => this.eventuallyRunOnInitialized(() => this.refreshProperties())); - this._postgresModel.onServiceUpdated(() => this.eventuallyRunOnInitialized(() => { - this.refreshProperties(); - this.refreshNodes(); - })); + modelView.onClosed(() => + this.disposables.forEach(d => { + try { d.dispose(); } + catch { } + })); - this._postgresModel.onPodsUpdated(() => this.eventuallyRunOnInitialized(() => { - this.refreshProperties(); - this.refreshNodes(); - })); + this.disposables.push(this._controllerModel.onEndpointsUpdated( + () => this.eventuallyRunOnInitialized(() => this.refreshEndpoints()))); + + this.disposables.push(this._controllerModel.onRegistrationsUpdated( + () => this.eventuallyRunOnInitialized(() => this.refreshProperties()))); + + this.disposables.push(this._postgresModel.onServiceUpdated( + () => this.eventuallyRunOnInitialized(() => { + this.refreshProperties(); + this.refreshNodes(); + }))); + + this.disposables.push(this._postgresModel.onPodsUpdated( + () => this.eventuallyRunOnInitialized(() => { + this.refreshProperties(); + this.refreshNodes(); + }))); } protected get title(): string { @@ -207,7 +221,7 @@ export class PostgresOverviewPage extends DashboardPage { vscode.window.showInformationMessage(loc.passwordReset); } } catch (error) { - vscode.window.showErrorMessage(loc.passwordResetFailed); + vscode.window.showErrorMessage(loc.passwordResetFailed(error)); } finally { resetPasswordButton.enabled = true; } diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts index ad154fd27f..dd24f3866c 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts @@ -13,12 +13,23 @@ import { ControllerModel } from '../../../models/controllerModel'; import { PostgresModel } from '../../../models/postgresModel'; export class PostgresPropertiesPage extends DashboardPage { + private disposables: vscode.Disposable[] = []; private keyValueContainer?: KeyValueContainer; constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) { super(modelView); - this._postgresModel.onServiceUpdated(() => this.eventuallyRunOnInitialized(() => this.refresh())); - this._controllerModel.onRegistrationsUpdated(() => this.eventuallyRunOnInitialized(() => this.refresh())); + + modelView.onClosed(() => + this.disposables.forEach(d => { + try { d.dispose(); } + catch { } + })); + + this.disposables.push(this._postgresModel.onServiceUpdated( + () => this.eventuallyRunOnInitialized(() => this.refresh()))); + + this.disposables.push(this._controllerModel.onRegistrationsUpdated( + () => this.eventuallyRunOnInitialized(() => this.refresh()))); } protected get title(): string { diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresResourceHealthPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresResourceHealthPage.ts new file mode 100644 index 0000000000..e828af562b --- /dev/null +++ b/extensions/arc/src/ui/dashboards/postgres/postgresResourceHealthPage.ts @@ -0,0 +1,192 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { DashboardPage } from '../../components/dashboardPage'; +import { PostgresModel } from '../../../models/postgresModel'; +import { fromNow } from '../../../common/date'; + +export class PostgresResourceHealthPage extends DashboardPage { + private disposables: vscode.Disposable[] = []; + private interval: NodeJS.Timeout; + private podsUpdated?: azdata.TextComponent; + private podsTable?: azdata.DeclarativeTableComponent; + private conditionsTable?: azdata.DeclarativeTableComponent; + + constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) { + super(modelView); + + modelView.onClosed(() => { + try { clearInterval(this.interval); } + catch { } + + this.disposables.forEach(d => { + try { d.dispose(); } + catch { } + }); + }); + + this.disposables.push(this._postgresModel.onServiceUpdated( + () => this.eventuallyRunOnInitialized(() => this.refresh()))); + + // Keep the last updated timestamps up to date with the current time + this.interval = setInterval(() => this.refresh(), 60 * 1000); + } + + protected get title(): string { + return loc.resourceHealth; + } + + protected get id(): string { + return 'postgres-resource-health'; + } + + protected get icon(): { dark: string; light: string; } { + return IconPathHelper.health; + } + + 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({ + value: loc.resourceHealth, + CSSStyles: { ...cssStyles.title, 'margin-bottom': '30px' } + }).component()); + + const titleCSS = { ...cssStyles.title, 'margin-block-start': '2em', 'margin-block-end': '0' }; + content.addItem(this.modelView.modelBuilder.text().withProperties({ + value: loc.podOverview, + CSSStyles: titleCSS + }).component()); + + this.podsUpdated = this.modelView.modelBuilder.text().component(); + content.addItem(this.podsUpdated); + + // Pod overview + this.podsTable = this.modelView.modelBuilder.declarativeTable().withProperties({ + columns: [ + { + displayName: '', + valueType: azdata.DeclarativeDataType.string, + width: '50%', + isReadOnly: true, + headerCssStyles: cssStyles.tableHeader, + rowCssStyles: { ...cssStyles.tableRow, 'font-size': '20px', 'font-weight': 'bold' } + }, + { + displayName: '', + valueType: azdata.DeclarativeDataType.string, + width: '50%', + isReadOnly: true, + headerCssStyles: cssStyles.tableHeader, + rowCssStyles: cssStyles.tableRow + } + ], + data: [] + }).component(); + content.addItem(this.podsTable, { CSSStyles: { 'margin-bottom': '30px' } }); + + // Conditions table + this.conditionsTable = this.modelView.modelBuilder.declarativeTable().withProperties({ + width: '100%', + columns: [ + { + displayName: loc.condition, + valueType: azdata.DeclarativeDataType.string, + width: '15%', + isReadOnly: true, + headerCssStyles: cssStyles.tableHeader, + rowCssStyles: cssStyles.tableRow + }, + { + displayName: '', + valueType: azdata.DeclarativeDataType.component, + width: '1%', + isReadOnly: true, + headerCssStyles: cssStyles.tableHeader, + rowCssStyles: cssStyles.tableRow + }, + { + displayName: loc.details, + valueType: azdata.DeclarativeDataType.string, + width: '64%', + isReadOnly: true, + headerCssStyles: cssStyles.tableHeader, + rowCssStyles: cssStyles.tableRow + }, + { + displayName: loc.lastUpdated, + valueType: azdata.DeclarativeDataType.string, + width: '20%', + isReadOnly: true, + headerCssStyles: cssStyles.tableHeader, + rowCssStyles: { ...cssStyles.tableRow, 'white-space': 'nowrap' } + } + ], + data: [] + }).component(); + content.addItem(this.conditionsTable); + + this.initialized = true; + return root; + } + + protected get toolbarContainer(): azdata.ToolbarContainer { + const refreshButton = this.modelView.modelBuilder.button().withProperties({ + label: loc.refresh, + iconPath: IconPathHelper.refresh + }).component(); + + refreshButton.onDidClick(async () => { + refreshButton.enabled = false; + try { + await this._postgresModel.refresh(); + } catch (error) { + vscode.window.showErrorMessage(loc.refreshFailed(error)); + } finally { + refreshButton.enabled = true; + } + }); + + return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([ + { component: refreshButton } + ]).component(); + } + + private refresh() { + this.podsUpdated!.value = loc.updated(fromNow(this._postgresModel.serviceLastUpdated!, true)); + + this.podsTable!.data = [ + [this._postgresModel.service?.status?.podsRunning, loc.running], + [this._postgresModel.service?.status?.podsPending, loc.pending], + [this._postgresModel.service?.status?.podsFailed, loc.failed], + [this._postgresModel.service?.status?.podsUnknown, loc.unknown] + ]; + + this.conditionsTable!.data = this._postgresModel.service?.status?.conditions?.map(c => { + const healthy = c.type === 'Ready' ? c.status === 'True' : c.status === 'False'; + + const image = this.modelView.modelBuilder.image().withProperties({ + iconPath: healthy ? IconPathHelper.success : IconPathHelper.fail, + iconHeight: '20px', + iconWidth: '20px', + width: '20px', + height: '20px' + }).component(); + + return [ + c.type, + image, + c.message, + fromNow(c.lastTransitionTime!, true) + ]; + }) ?? []; + } +}