diff --git a/extensions/arc/src/ui/components/keyValueContainer.ts b/extensions/arc/src/ui/components/keyValueContainer.ts index db39469d24..63758f9edb 100644 --- a/extensions/arc/src/ui/components/keyValueContainer.ts +++ b/extensions/arc/src/ui/components/keyValueContainer.ts @@ -9,115 +9,160 @@ import * as loc from '../../localizedConstants'; import { IconPathHelper, cssStyles } from '../../constants'; /** A container with a single vertical column of KeyValue pairs */ -export class KeyValueContainer { +export class KeyValueContainer extends vscode.Disposable { public container: azdata.DivContainer; - private keyToComponent: Map; + private pairs: KeyValue[] = []; + + constructor(modelBuilder: azdata.ModelBuilder, pairs: KeyValue[]) { + super(() => this.pairs.forEach(d => { + try { d.dispose(); } catch { } + })); - constructor(private modelBuilder: azdata.ModelBuilder, pairs: KeyValue[]) { this.container = modelBuilder.divContainer().component(); - this.keyToComponent = new Map(); this.refresh(pairs); } - // TODO: Support removing KeyValues, and handle race conditions when - // adding/removing KeyValues concurrently. For now this should only be used - // when the set of keys won't change (though their values can be refreshed). + // TODO: Support adding/removing KeyValues concurrently. For now this should only + // be used when the set of keys won't change (though their values can be refreshed). public refresh(pairs: KeyValue[]) { - pairs.forEach(p => { - let component = this.keyToComponent.get(p.key); - if (component) { - component.value = p.value; - } else { - component = p.getComponent(this.modelBuilder); - this.keyToComponent.set(p.key, component); + pairs.forEach(newPair => { + const pair = this.pairs.find(oldPair => oldPair.key === newPair.key); + if (!pair) { + this.pairs.push(newPair); this.container.addItem( - component, - { CSSStyles: { 'margin-bottom': '15px', 'min-height': '30px' } } - ); + newPair.container, + { CSSStyles: { 'margin-bottom': '15px', 'min-height': '30px' } }); + } else if (pair.value !== newPair.value) { + pair.setValue(newPair.value); } }); } } /** A key value pair in the KeyValueContainer */ -export abstract class KeyValue { - constructor(public key: string, public value: string) { } +export abstract class KeyValue extends vscode.Disposable { + readonly container: azdata.FlexContainer; + protected disposables: vscode.Disposable[] = []; + protected valueFlex = { flex: '1 1 250px' }; + private keyFlex = { flex: `0 0 200px` }; - /** 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({ - value: this.key, + constructor(modelBuilder: azdata.ModelBuilder, readonly key: string, private _value: string) { + super(() => this.disposables.forEach(d => { + try { d.dispose(); } catch { } + })); + + this.container = modelBuilder.flexContainer().withLayout({ + flexWrap: 'wrap', + alignItems: 'center' + }).component(); + + const keyComponent = modelBuilder.text().withProperties({ + value: 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; + this.container.addItem(keyComponent, this.keyFlex); } - /** Returns a component representing the value of the KeyValue pair */ - protected abstract getValueComponent(modelBuilder: azdata.ModelBuilder): azdata.Component; + get value(): string { + return this._value; + } + + public setValue(newValue: string) { + this._value = newValue; + } } /** Implementation of KeyValue where the value is text */ export class TextKeyValue extends KeyValue { - getValueComponent(modelBuilder: azdata.ModelBuilder): azdata.Component { - return modelBuilder.text().withProperties({ - value: this.value, + private text: azdata.TextComponent; + + constructor(modelBuilder: azdata.ModelBuilder, key: string, value: string) { + super(modelBuilder, key, value); + + this.text = modelBuilder.text().withProperties({ + value: value, CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' } }).component(); + + this.container.addItem(this.text, this.valueFlex); + } + + public setValue(newValue: string) { + super.setValue(newValue); + this.text.value = newValue; } } /** Implementation of KeyValue where the value is a readonly copyable input field */ export abstract class BaseInputKeyValue extends KeyValue { - constructor(key: string, value: string, private multiline: boolean) { super(key, value); } + private input: azdata.InputBoxComponent; - getValueComponent(modelBuilder: azdata.ModelBuilder): azdata.Component { - const container = modelBuilder.flexContainer().withLayout({ alignItems: 'center' }).component(); - container.addItem(modelBuilder.inputBox().withProperties({ - value: this.value, + constructor(modelBuilder: azdata.ModelBuilder, key: string, value: string, multiline: boolean) { + super(modelBuilder, key, value); + + this.input = modelBuilder.inputBox().withProperties({ + value: value, readOnly: true, - multiline: this.multiline - }).component()); - - const copy = modelBuilder.button().withProperties({ - iconPath: IconPathHelper.copy, width: '17px', height: '17px' + multiline: multiline }).component(); - copy.onDidClick(async () => { - vscode.env.clipboard.writeText(this.value); - vscode.window.showInformationMessage(loc.copiedToClipboard(this.key)); - }); + const inputContainer = modelBuilder.flexContainer().withLayout({ alignItems: 'center' }).component(); + inputContainer.addItem(this.input); - container.addItem(copy, { CSSStyles: { 'margin-left': '10px' } }); - return container; + const copy = modelBuilder.button().withProperties({ + iconPath: IconPathHelper.copy, + width: '17px', + height: '17px' + }).component(); + + this.disposables.push(copy.onDidClick(async () => { + vscode.env.clipboard.writeText(value); + vscode.window.showInformationMessage(loc.copiedToClipboard(key)); + })); + + inputContainer.addItem(copy, { CSSStyles: { 'margin-left': '10px' } }); + this.container.addItem(inputContainer, this.valueFlex); + } + + public setValue(newValue: string) { + super.setValue(newValue); + this.input.value = newValue; } } /** Implementation of KeyValue where the value is a single line readonly copyable input field */ export class InputKeyValue extends BaseInputKeyValue { - constructor(key: string, value: string) { super(key, value, false); } + constructor(modelBuilder: azdata.ModelBuilder, key: string, value: string) { + super(modelBuilder, key, value, false); + } } /** Implementation of KeyValue where the value is a multi line readonly copyable input field */ export class MultilineInputKeyValue extends BaseInputKeyValue { - constructor(key: string, value: string) { super(key, value, true); } + constructor(modelBuilder: azdata.ModelBuilder, key: string, value: string) { + super(modelBuilder, key, value, true); + } } /** 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); - } + private link: azdata.HyperlinkComponent; - getValueComponent(modelBuilder: azdata.ModelBuilder): azdata.Component { - const link = modelBuilder.hyperlink().withProperties({ - label: this.value, url: '' + constructor(modelBuilder: azdata.ModelBuilder, key: string, value: string, onClick: (e: any) => any) { + super(modelBuilder, key, value); + + this.link = modelBuilder.hyperlink().withProperties({ + label: value, + url: '' }).component(); - link.onDidClick(this.onClick); - return link; + this.disposables.push(this.link.onDidClick(onClick)); + this.container.addItem(this.link, this.valueFlex); + } + + public setValue(newValue: string) { + super.setValue(newValue); + this.link.label = newValue; } } diff --git a/extensions/arc/src/ui/dashboards/miaa/miaaConnectionStringsPage.ts b/extensions/arc/src/ui/dashboards/miaa/miaaConnectionStringsPage.ts index d6fc936235..22bc3ab20c 100644 --- a/extensions/arc/src/ui/dashboards/miaa/miaaConnectionStringsPage.ts +++ b/extensions/arc/src/ui/dashboards/miaa/miaaConnectionStringsPage.ts @@ -56,7 +56,9 @@ export class MiaaConnectionStringsPage extends DashboardPage { { CSSStyles: { display: 'inline-flex', 'margin-bottom': '25px' } }); this._keyValueContainer = new KeyValueContainer(this.modelView.modelBuilder, []); + this.disposables.push(this._keyValueContainer); content.addItem(this._keyValueContainer.container); + this.updateConnectionStrings(); this.initialized = true; return root; @@ -76,18 +78,18 @@ export class MiaaConnectionStringsPage extends DashboardPage { const username = this._miaaModel.username; const pairs: KeyValue[] = [ - new InputKeyValue('ADO.NET', `Server=tcp:${ip},${port};Persist Security Info=False;User ID=${username};Password={your_password_here};MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;`), - new InputKeyValue('C++ (libpq)', `host=${ip} port=${port} user=${username} password={your_password_here} sslmode=require`), - new InputKeyValue('JDBC', `jdbc:sqlserver://${ip}:${port};user=${username};password={your_password_here};encrypt=true;trustServerCertificate=false;loginTimeout=30;`), - new InputKeyValue('Node.js', `host=${ip} port=${port} dbname=master user=${username} password={your_password_here} sslmode=require`), - new InputKeyValue('ODBC', `Driver={ODBC Driver 13 for SQL Server};Server=${ip},${port};Uid=${username};Pwd={your_password_here};Encrypt=yes;TrustServerCertificate=no;Connection Timeout=30;`), - new MultilineInputKeyValue('PHP', + new InputKeyValue(this.modelView.modelBuilder, 'ADO.NET', `Server=tcp:${ip},${port};Persist Security Info=False;User ID=${username};Password={your_password_here};MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;`), + new InputKeyValue(this.modelView.modelBuilder, 'C++ (libpq)', `host=${ip} port=${port} user=${username} password={your_password_here} sslmode=require`), + new InputKeyValue(this.modelView.modelBuilder, 'JDBC', `jdbc:sqlserver://${ip}:${port};user=${username};password={your_password_here};encrypt=true;trustServerCertificate=false;loginTimeout=30;`), + new InputKeyValue(this.modelView.modelBuilder, 'Node.js', `host=${ip} port=${port} dbname=master user=${username} password={your_password_here} sslmode=require`), + new InputKeyValue(this.modelView.modelBuilder, 'ODBC', `Driver={ODBC Driver 13 for SQL Server};Server=${ip},${port};Uid=${username};Pwd={your_password_here};Encrypt=yes;TrustServerCertificate=no;Connection Timeout=30;`), + new MultilineInputKeyValue(this.modelView.modelBuilder, 'PHP', `$connectionInfo = array("UID" => "${username}", "pwd" => "{your_password_here}", "LoginTimeout" => 30, "Encrypt" => 1, "TrustServerCertificate" => 0); $serverName = "${ip},${port}"; $conn = sqlsrv_connect($serverName, $connectionInfo);`), - new InputKeyValue('Python', `dbname='master' user='${username}' host='${ip}' password='{your_password_here}' port='${port}' sslmode='true'`), - new InputKeyValue('Ruby', `host=${ip}; user=${username} password={your_password_here} port=${port} sslmode=require`), - new InputKeyValue('Web App', `Database=master; Data Source=${ip}; User Id=${username}; Password={your_password_here}`) + new InputKeyValue(this.modelView.modelBuilder, 'Python', `dbname='master' user='${username}' host='${ip}' password='{your_password_here}' port='${port}' sslmode='true'`), + new InputKeyValue(this.modelView.modelBuilder, 'Ruby', `host=${ip}; user=${username} password={your_password_here} port=${port} sslmode=require`), + new InputKeyValue(this.modelView.modelBuilder, 'Web App', `Database=master; Data Source=${ip}; User Id=${username}; Password={your_password_here}`) ]; this._keyValueContainer.refresh(pairs); } diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts index bbaf84fc4b..50857543eb 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts @@ -60,6 +60,8 @@ export class PostgresConnectionStringsPage extends DashboardPage { content.addItem(infoAndLink, { CSSStyles: { 'margin-bottom': '25px' } }); this.keyValueContainer = new KeyValueContainer(this.modelView.modelBuilder, this.getConnectionStrings()); + this.disposables.push(this.keyValueContainer); + this.loading = this.modelView.modelBuilder.loadingComponent() .withItem(this.keyValueContainer.container) .withProperties({ @@ -99,15 +101,15 @@ export class PostgresConnectionStringsPage extends DashboardPage { const endpoint: { ip?: string, port?: number } = this._postgresModel.endpoint; return [ - new InputKeyValue('ADO.NET', `Server=${endpoint.ip};Database=postgres;Port=${endpoint.port};User Id=postgres;Password={your_password_here};Ssl Mode=Require;`), - new InputKeyValue('C++ (libpq)', `host=${endpoint.ip} port=${endpoint.port} dbname=postgres user=postgres password={your_password_here} sslmode=require`), - new InputKeyValue('JDBC', `jdbc:postgresql://${endpoint.ip}:${endpoint.port}/postgres?user=postgres&password={your_password_here}&sslmode=require`), - new InputKeyValue('Node.js', `host=${endpoint.ip} port=${endpoint.port} dbname=postgres user=postgres password={your_password_here} sslmode=require`), - new InputKeyValue('PHP', `host=${endpoint.ip} port=${endpoint.port} dbname=postgres user=postgres password={your_password_here} sslmode=require`), - new InputKeyValue('psql', `psql "host=${endpoint.ip} port=${endpoint.port} dbname=postgres user=postgres password={your_password_here} sslmode=require"`), - new InputKeyValue('Python', `dbname='postgres' user='postgres' host='${endpoint.ip}' password='{your_password_here}' port='${endpoint.port}' sslmode='true'`), - new InputKeyValue('Ruby', `host=${endpoint.ip}; dbname=postgres user=postgres password={your_password_here} port=${endpoint.port} sslmode=require`), - new InputKeyValue('Web App', `Database=postgres; Data Source=${endpoint.ip}; User Id=postgres; Password={your_password_here}`) + new InputKeyValue(this.modelView.modelBuilder, 'ADO.NET', `Server=${endpoint.ip};Database=postgres;Port=${endpoint.port};User Id=postgres;Password={your_password_here};Ssl Mode=Require;`), + new InputKeyValue(this.modelView.modelBuilder, 'C++ (libpq)', `host=${endpoint.ip} port=${endpoint.port} dbname=postgres user=postgres password={your_password_here} sslmode=require`), + new InputKeyValue(this.modelView.modelBuilder, 'JDBC', `jdbc:postgresql://${endpoint.ip}:${endpoint.port}/postgres?user=postgres&password={your_password_here}&sslmode=require`), + new InputKeyValue(this.modelView.modelBuilder, 'Node.js', `host=${endpoint.ip} port=${endpoint.port} dbname=postgres user=postgres password={your_password_here} sslmode=require`), + new InputKeyValue(this.modelView.modelBuilder, 'PHP', `host=${endpoint.ip} port=${endpoint.port} dbname=postgres user=postgres password={your_password_here} sslmode=require`), + new InputKeyValue(this.modelView.modelBuilder, 'psql', `psql "host=${endpoint.ip} port=${endpoint.port} dbname=postgres user=postgres password={your_password_here} sslmode=require"`), + new InputKeyValue(this.modelView.modelBuilder, 'Python', `dbname='postgres' user='postgres' host='${endpoint.ip}' password='{your_password_here}' port='${endpoint.port}' sslmode='true'`), + new InputKeyValue(this.modelView.modelBuilder, 'Ruby', `host=${endpoint.ip}; dbname=postgres user=postgres password={your_password_here} port=${endpoint.port} sslmode=require`), + new InputKeyValue(this.modelView.modelBuilder, 'Web App', `Database=postgres; Data Source=${endpoint.ip}; User Id=postgres; Password={your_password_here}`) ]; } diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts index 92542b030a..9d0a7abe68 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts @@ -49,6 +49,8 @@ export class PostgresPropertiesPage extends DashboardPage { }).component()); this.keyValueContainer = new KeyValueContainer(this.modelView.modelBuilder, this.getProperties()); + this.disposables.push(this.keyValueContainer); + this.loading = this.modelView.modelBuilder.loadingComponent() .withItem(this.keyValueContainer.container) .withProperties({ @@ -94,15 +96,15 @@ export class PostgresPropertiesPage extends DashboardPage { const registration = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name); return [ - new InputKeyValue(loc.coordinatorEndpoint, connectionString), - new InputKeyValue(loc.postgresAdminUsername, 'postgres'), - new TextKeyValue(loc.status, this._postgresModel.service?.status?.state ?? 'Unknown'), + new InputKeyValue(this.modelView.modelBuilder, loc.coordinatorEndpoint, connectionString), + new InputKeyValue(this.modelView.modelBuilder, loc.postgresAdminUsername, 'postgres'), + new TextKeyValue(this.modelView.modelBuilder, loc.status, this._postgresModel.service?.status?.state ?? 'Unknown'), // TODO: Make this a LinkKeyValue that opens the controller dashboard - new TextKeyValue(loc.dataController, this._controllerModel.namespace ?? ''), - new TextKeyValue(loc.nodeConfiguration, this._postgresModel.configuration), - new TextKeyValue(loc.postgresVersion, this._postgresModel.service?.spec?.engine?.version?.toString() ?? ''), - new TextKeyValue(loc.resourceGroup, registration?.resourceGroupName ?? ''), - new TextKeyValue(loc.subscriptionId, registration?.subscriptionId ?? '') + new TextKeyValue(this.modelView.modelBuilder, loc.dataController, this._controllerModel.namespace ?? ''), + new TextKeyValue(this.modelView.modelBuilder, loc.nodeConfiguration, this._postgresModel.configuration), + new TextKeyValue(this.modelView.modelBuilder, loc.postgresVersion, this._postgresModel.service?.spec?.engine?.version?.toString() ?? ''), + new TextKeyValue(this.modelView.modelBuilder, loc.resourceGroup, registration?.resourceGroupName ?? ''), + new TextKeyValue(this.modelView.modelBuilder, loc.subscriptionId, registration?.subscriptionId ?? '') ]; }