diff --git a/addons/api/mirage/scenarios/ipc.js b/addons/api/mirage/scenarios/ipc.js
index 652459391..62e17c9c5 100644
--- a/addons/api/mirage/scenarios/ipc.js
+++ b/addons/api/mirage/scenarios/ipc.js
@@ -199,18 +199,18 @@ export default function initializeMockIPC(server, config) {
getDesktopVersion() {
return { desktopVersion: '1.0.0' };
}
-
getLogLevel() {
- return 'debug';
+ return 'info';
}
-
- setLogLevel() {
- return 'warn';
- }
-
getLogPath() {
return '~/.config/Boundary/logs/desktop-client.log';
}
+ setLogLevel() {}
+ clientAgentStatus() {
+ return { version: '0.0.1-dev', status: 'running' };
+ }
+ pauseClientAgent() {}
+ resumeClientAgent() {}
}
/**
diff --git a/addons/core/translations/actions/en-us.yaml b/addons/core/translations/actions/en-us.yaml
index 6ab0b3bbe..f23351b08 100644
--- a/addons/core/translations/actions/en-us.yaml
+++ b/addons/core/translations/actions/en-us.yaml
@@ -22,6 +22,8 @@ back: Back
view: View
retry: Retry
play: Play
+pause: Pause
+resume: Resume
get-topic-help: Get help for this topic
copy-to-clipboard: Copy to Clipboard
copy-error-detail-to-clipboard: Copy error detail to clipboard
diff --git a/addons/core/translations/en-us.yaml b/addons/core/translations/en-us.yaml
index 4d642b5ef..8cf9176cf 100644
--- a/addons/core/translations/en-us.yaml
+++ b/addons/core/translations/en-us.yaml
@@ -109,7 +109,11 @@ settings:
application: Application
cli: CLI
cache-daemon: Cache Daemon
+ client-agent:
+ title: Boundary Client Agent
+ description: Required to use Transparent Sessions
client: Client
+ version: Version
color-theme: Color Theme
provider: Provider
hcp: HashiCorp Cloud Platform
@@ -127,3 +131,6 @@ settings:
warn: Warn
info: Info
debug: Debug
+ alerts:
+ cache-daemon: There may be a problem with the cache daemon
+ client-agent: There may be a problem with the client agent
diff --git a/addons/core/translations/errors/en-us.yaml b/addons/core/translations/errors/en-us.yaml
index 7c5207a33..525883d68 100644
--- a/addons/core/translations/errors/en-us.yaml
+++ b/addons/core/translations/errors/en-us.yaml
@@ -27,5 +27,3 @@ onboarding-failed:
required-field: This is a required field.
client-agent-failed:
sessions: Unable to retrieve sessions from the client agent.
-settings-cache:
- title: There may be a problem with the cache daemon
diff --git a/addons/core/translations/states/en-us.yaml b/addons/core/translations/states/en-us.yaml
index 15893d59d..c3d36f289 100644
--- a/addons/core/translations/states/en-us.yaml
+++ b/addons/core/translations/states/en-us.yaml
@@ -17,6 +17,7 @@ failed: Failed
installed: Installed
not-running: Not Running
running: Running
+paused: Paused
authenticated: Authenticated
unauthenticated: Unauthenticated
loading:
diff --git a/ui/desktop/app/components/settings-card/application/index.hbs b/ui/desktop/app/components/settings-card/application/index.hbs
index 1e91984fe..b8539a6b1 100644
--- a/ui/desktop/app/components/settings-card/application/index.hbs
+++ b/ui/desktop/app/components/settings-card/application/index.hbs
@@ -10,7 +10,7 @@
{{t 'settings.client'}}
- {{@model.formattedDesktopVersion}}
+ {{@model.desktopVersion}}
@@ -18,7 +18,7 @@
{{t 'settings.cli'}}
- {{@model.formattedCliVersion}}
+ {{@model.cliVersion}}
@@ -26,8 +26,8 @@
{{t 'settings.cache-daemon'}}
- {{#if @model.formattedCacheVersion}}
- {{@model.formattedCacheVersion}}
+ {{#if @model.cacheDaemonStatus.version}}
+ {{@model.cacheDaemonStatus.version}}
- {{#if @model.cacheDaemonErrors}}
+ {{#if @model.cacheDaemonStatus.errors}}
- {{t 'errors.settings-cache.title'}}
+ {{t 'settings.alerts.cache-daemon'}}
- {{#each @model.cacheDaemonErrors as |error|}}
+ {{#each @model.cacheDaemonStatus.errors as |error|}}
{{#if error.name}}
{{error.name}}:
diff --git a/ui/desktop/app/components/settings-card/client-agent/index.hbs b/ui/desktop/app/components/settings-card/client-agent/index.hbs
new file mode 100644
index 000000000..b04aec8b9
--- /dev/null
+++ b/ui/desktop/app/components/settings-card/client-agent/index.hbs
@@ -0,0 +1,63 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: BUSL-1.1
+}}
+
+{{#if this.isClientAgentActive}}
+
+ <:state>
+ {{#if this.isClientAgentStatusRunning}}
+
+ {{else}}
+
+ {{/if}}
+
+ <:body>
+
+
+ {{t 'settings.version'}}
+
+
+ {{@model.clientAgentStatus.version}}
+
+
+ {{#if @model.clientAgentStatus.errors}}
+
+ {{t 'settings.alerts.client-agent'}}
+
+ {{#each @model.clientAgentStatus.errors as |error|}}
+
+ {{error.message}}
+
+ {{/each}}
+
+
+ {{/if}}
+
+
+ <:action>
+
+
+
+{{/if}}
\ No newline at end of file
diff --git a/ui/desktop/app/components/settings-card/client-agent/index.js b/ui/desktop/app/components/settings-card/client-agent/index.js
new file mode 100644
index 000000000..2ba238c34
--- /dev/null
+++ b/ui/desktop/app/components/settings-card/client-agent/index.js
@@ -0,0 +1,68 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Component from '@glimmer/component';
+import { inject as service } from '@ember/service';
+import { action } from '@ember/object';
+import { loading } from 'ember-loading';
+import { notifyError } from 'core/decorators/notify';
+
+export default class SettingsCardClientAgentComponent extends Component {
+ // =services
+ @service ipc;
+ @service router;
+ @service loading;
+
+ // =attributes
+
+ /**
+ * Checks if the status returned by the client agent is "running"
+ * @return {boolean}
+ */
+ get isClientAgentStatusRunning() {
+ return this.args.model.clientAgentStatus?.status === 'running';
+ }
+
+ /**
+ * Checks if the client agent is actively running
+ * @return {boolean}
+ */
+ get isClientAgentActive() {
+ return (
+ this.args.model.clientAgentStatus?.status === 'running' ||
+ this.args.model.clientAgentStatus?.status === 'paused'
+ );
+ }
+
+ /**
+ * Return the button icon based on the current state of the client agent
+ * @return {string}
+ */
+ get buttonIcon() {
+ if (this.loading.isLoading) {
+ return 'loading';
+ } else {
+ return this.isClientAgentStatusRunning ? 'pause' : 'play';
+ }
+ }
+
+ // =actions
+
+ /**
+ * Changes the state of the client agent based on the current state.
+ * @return {Promise}
+ */
+ @action
+ @loading
+ @notifyError(({ message }) => message)
+ async changeClientAgentState() {
+ if (this.isClientAgentStatusRunning) {
+ await this.ipc.invoke('pauseClientAgent');
+ } else {
+ await this.ipc.invoke('resumeClientAgent');
+ }
+ await this.router.refresh();
+ }
+}
diff --git a/ui/desktop/app/components/settings-card/index.hbs b/ui/desktop/app/components/settings-card/index.hbs
index 80d79e3d4..5694a12c1 100644
--- a/ui/desktop/app/components/settings-card/index.hbs
+++ b/ui/desktop/app/components/settings-card/index.hbs
@@ -4,7 +4,6 @@
}}
-
@@ -20,11 +19,10 @@
{{yield to='action'}}
{{/if}}
-
{{#if @description}}
-
+
{{@description}}
{{/if}}
@@ -35,5 +33,4 @@
{{yield to='body'}}
{{/if}}
-
\ No newline at end of file
diff --git a/ui/desktop/app/components/settings-card/logs/index.js b/ui/desktop/app/components/settings-card/logs/index.js
index 22b26234e..40a14c4e5 100644
--- a/ui/desktop/app/components/settings-card/logs/index.js
+++ b/ui/desktop/app/components/settings-card/logs/index.js
@@ -13,11 +13,21 @@ export default class SettingsCardLogsComponent extends Component {
// =attributes
+ /**
+ * Returns the list of available log levels
+ * @return {string[]}
+ */
get logLevels() {
return ['error', 'warn', 'info', 'debug'];
}
// =methods
+
+ /**
+ * Sets the log level to the selected value
+ * @param value
+ * @return {Promise}
+ */
@action
async selectLogLevel({ target: { value } }) {
await this.ipc.invoke('setLogLevel', value);
diff --git a/ui/desktop/app/components/settings-card/user/index.js b/ui/desktop/app/components/settings-card/user/index.js
index 917437b6f..0c946ae30 100644
--- a/ui/desktop/app/components/settings-card/user/index.js
+++ b/ui/desktop/app/components/settings-card/user/index.js
@@ -13,6 +13,7 @@ export default class SettingsUserInfoComponent extends Component {
@service session;
@service intl;
+ // =attributes
/**
* Returns the username of the current user
* @type {string}
diff --git a/ui/desktop/app/routes/scopes/scope/projects.js b/ui/desktop/app/routes/scopes/scope/projects.js
index d8f6e0bbb..a4160ed1c 100644
--- a/ui/desktop/app/routes/scopes/scope/projects.js
+++ b/ui/desktop/app/routes/scopes/scope/projects.js
@@ -64,6 +64,8 @@ export default class ScopesScopeProjectsRoute extends Route {
);
if (isClientAgentRunning) {
this.job.start();
+ } else {
+ this.job.stop();
}
}
diff --git a/ui/desktop/app/routes/scopes/scope/projects/settings/index.js b/ui/desktop/app/routes/scopes/scope/projects/settings/index.js
index 09fb66aae..4f4766c6c 100644
--- a/ui/desktop/app/routes/scopes/scope/projects/settings/index.js
+++ b/ui/desktop/app/routes/scopes/scope/projects/settings/index.js
@@ -14,26 +14,38 @@ export default class ScopesScopeProjectsSettingsIndexRoute extends Route {
// =methods
async model() {
- let cacheDaemonStatus,
- formattedCacheVersion,
- formattedCliVersion,
- formattedDesktopVersion,
- cacheDaemonErrors = [];
-
const { versionNumber: cliVersion } =
await this.ipc.invoke('getCliVersion');
- formattedCliVersion = `v${cliVersion}`;
const { desktopVersion } = await this.ipc.invoke('getDesktopVersion');
- formattedDesktopVersion = `v${desktopVersion}`;
+ const cacheDaemonStatus = await this.getCacheStatus();
+ const clientAgentStatus = await this.getClientAgentStatus();
+ const logLevel = await this.ipc.invoke('getLogLevel');
+ const logPath = await this.ipc.invoke('getLogPath');
+ const serverInformation = this.clusterUrl.rendererClusterUrl;
+
+ return {
+ desktopVersion: `v${desktopVersion}`,
+ cliVersion: `v${cliVersion}`,
+ cacheDaemonStatus,
+ clientAgentStatus,
+ logLevel,
+ logPath,
+ serverInformation,
+ };
+ }
+
+ async getCacheStatus() {
+ let cacheDaemonStatus;
+ let cacheDaemonStatusError = [];
try {
cacheDaemonStatus = await this.ipc.invoke('cacheDaemonStatus');
} catch (e) {
- cacheDaemonErrors.push(e);
+ cacheDaemonStatusError.push(e);
}
const { version, users } = cacheDaemonStatus ?? {};
- formattedCacheVersion = version?.match(/v\d+\.\d+\.\d+/)?.[0];
+ const cacheVersion = version?.match(/v\d+\.\d+\.\d+/)?.[0];
// Grab the errors from each resource
const userId = this.session.data.authenticated.user_id;
@@ -45,20 +57,24 @@ export default class ScopesScopeProjectsSettingsIndexRoute extends Route {
message: resource.last_error.error,
name: resource.name,
})) ?? [];
- cacheDaemonErrors = [...cacheDaemonErrors, ...resourceErrors];
- const logLevel = await this.ipc.invoke('getLogLevel');
- const logPath = await this.ipc.invoke('getLogPath');
- const serverInformation = this.clusterUrl.rendererClusterUrl;
+ const errors = [...cacheDaemonStatusError, ...resourceErrors];
+ return { version: cacheVersion, errors };
+ }
- return {
- cacheDaemonErrors,
- formattedCliVersion,
- formattedCacheVersion,
- formattedDesktopVersion,
- logLevel,
- logPath,
- serverInformation,
- };
+ async getClientAgentStatus() {
+ let clientAgentStatus;
+ let clientAgentStatusError = [];
+ try {
+ clientAgentStatus = await this.ipc.invoke('clientAgentStatus');
+ } catch (e) {
+ clientAgentStatusError.push(e);
+ }
+
+ const { version, status, errors: errorMessages } = clientAgentStatus ?? {};
+ const statusErrors = errorMessages?.map((message) => ({ message })) ?? [];
+
+ const errors = [...clientAgentStatusError, ...statusErrors];
+ return { version: `v${version}`, status, errors };
}
}
diff --git a/ui/desktop/app/templates/scopes/scope/projects/settings/index.hbs b/ui/desktop/app/templates/scopes/scope/projects/settings/index.hbs
index 1d07b63a8..d7b5cd04a 100644
--- a/ui/desktop/app/templates/scopes/scope/projects/settings/index.hbs
+++ b/ui/desktop/app/templates/scopes/scope/projects/settings/index.hbs
@@ -4,6 +4,7 @@
}}
+
\ No newline at end of file
diff --git a/ui/desktop/electron-app/src/helpers/spawn-promise.js b/ui/desktop/electron-app/src/helpers/spawn-promise.js
index bf6c7e996..8835509e0 100644
--- a/ui/desktop/electron-app/src/helpers/spawn-promise.js
+++ b/ui/desktop/electron-app/src/helpers/spawn-promise.js
@@ -99,10 +99,11 @@ module.exports = {
* Spawn a child process and return a promise.
* Resolves on any output from stdout or stderr.
* @param command
+ * @param options
*/
- spawn(command) {
+ spawn(command, options) {
return new Promise((resolve, reject) => {
- const childProcess = spawn(path(), command);
+ const childProcess = spawn(path(), command, options);
childProcess.stdout.on('data', (data) => {
resolve({ childProcess, stdout: data.toString() });
});
diff --git a/ui/desktop/electron-app/src/ipc/handlers.js b/ui/desktop/electron-app/src/ipc/handlers.js
index f529ffae1..6cd8263fa 100644
--- a/ui/desktop/electron-app/src/ipc/handlers.js
+++ b/ui/desktop/electron-app/src/ipc/handlers.js
@@ -197,6 +197,21 @@ handle('isClientAgentRunning', async () => {
}
});
+/**
+ * Returns the client agent's status
+ */
+handle('clientAgentStatus', async () => clientAgentDaemonManager.status());
+
+/**
+ * Pauses the client agent
+ */
+handle('pauseClientAgent', async () => clientAgentDaemonManager.pause());
+
+/**
+ * Resumes the client agent
+ */
+handle('resumeClientAgent', async () => clientAgentDaemonManager.resume());
+
/**
* Gets boundary cli version
*/
@@ -210,7 +225,7 @@ handle('getDesktopVersion', async () => ({ desktopVersion: releaseVersion }));
/**
* Returns the Cache daemon status
*/
-handle('cacheDaemonStatus', async () => await cacheDaemonManager.status());
+handle('cacheDaemonStatus', async () => cacheDaemonManager.status());
/**
* Returns the current log level
diff --git a/ui/desktop/electron-app/src/services/client-agent-daemon-manager.js b/ui/desktop/electron-app/src/services/client-agent-daemon-manager.js
index 9111f2bd9..9764ff712 100644
--- a/ui/desktop/electron-app/src/services/client-agent-daemon-manager.js
+++ b/ui/desktop/electron-app/src/services/client-agent-daemon-manager.js
@@ -15,7 +15,11 @@ class ClientAgentDaemonManager {
*/
async status() {
const clientAgentStatusCommand = ['client-agent', 'status', '-format=json'];
- const { stdout, stderr } = spawnSync(clientAgentStatusCommand);
+ // The status command can take a while to execute if it's not running as it
+ // retries multiple times so we should set a timeout to not wait too long
+ const { stderr, stdout } = await spawn(clientAgentStatusCommand, {
+ timeout: 500,
+ });
let parsedResponse = jsonify(stdout);
if (parsedResponse?.status_code === 200) {
@@ -47,6 +51,46 @@ class ClientAgentDaemonManager {
log.warn('Client Agent Sessions:', stderr);
return generateErrorPromise(stderr);
}
+
+ /**
+ * Pauses the client agent daemon.
+ * @return {Promise<*>}
+ */
+ async pause() {
+ const clientAgentPauseCommand = ['client-agent', 'pause', '-format=json'];
+ // We use spawn here as pausing can take a while to execute and using spawnSync
+ // can cause the app to completely freeze and be unresponsive during that time
+ const { stderr, stdout } = await spawn(clientAgentPauseCommand, {
+ timeout: 10000,
+ });
+
+ let parsedResponse = jsonify(stdout);
+ if (parsedResponse?.status_code === 200) {
+ return parsedResponse.item;
+ }
+
+ log.warn('Client Agent Pause:', stderr);
+ return generateErrorPromise(stderr);
+ }
+
+ /**
+ * Resumes the client agent daemon.
+ * @return {Promise<*>}
+ */
+ async resume() {
+ const clientAgentResumeCommand = ['client-agent', 'resume', '-format=json'];
+ const { stderr, stdout } = await spawn(clientAgentResumeCommand, {
+ timeout: 10000,
+ });
+
+ let parsedResponse = jsonify(stdout);
+ if (parsedResponse?.status_code === 200) {
+ return parsedResponse.item;
+ }
+
+ log.warn('Client Agent Resume:', stderr);
+ return generateErrorPromise(stderr);
+ }
}
// Export an instance so we get a singleton
diff --git a/ui/desktop/tests/helpers/window-mock-ipc.js b/ui/desktop/tests/helpers/window-mock-ipc.js
index 5f1244e29..d48fef131 100644
--- a/ui/desktop/tests/helpers/window-mock-ipc.js
+++ b/ui/desktop/tests/helpers/window-mock-ipc.js
@@ -34,6 +34,27 @@ class MockIPC {
isClientAgentRunning() {
return false;
}
+ cacheDaemonStatus() {
+ return { version: 'Boundary v1.0.0' };
+ }
+ getCliVersion() {
+ return { versionNumber: '1.0.0' };
+ }
+ getDesktopVersion() {
+ return { desktopVersion: '1.0.0' };
+ }
+ getLogLevel() {
+ return 'info';
+ }
+ getLogPath() {
+ return '~/.config/Boundary/logs/desktop-client.log';
+ }
+ setLogLevel() {}
+ clientAgentStatus() {
+ return { version: '0.0.1-dev', status: 'running' };
+ }
+ pauseClientAgent() {}
+ resumeClientAgent() {}
}
/**
diff --git a/ui/desktop/tests/integration/components/settings-card/application-test.js b/ui/desktop/tests/integration/components/settings-card/application-test.js
index afed0dc8f..2c59ecd37 100644
--- a/ui/desktop/tests/integration/components/settings-card/application-test.js
+++ b/ui/desktop/tests/integration/components/settings-card/application-test.js
@@ -18,13 +18,18 @@ module('Integration | Component | settings-card/application', function (hooks) {
setupRenderingTest(hooks);
setupIntl(hooks);
+ let model;
+
+ hooks.beforeEach(() => {
+ model = {
+ cacheDaemonStatus: { version: 'v4.0.0', errors: [] },
+ desktopVersion: 'v2.0.0',
+ cliVersion: 'v3.0.0',
+ };
+ });
+
test('it renders', async function (assert) {
- this.set('model', {
- cacheDaemonErrors: [],
- formattedDesktopVersion: 'v2.0.0',
- formattedCliVersion: 'v3.0.0',
- formattedCacheVersion: 'v4.0.0',
- });
+ this.set('model', model);
this.set('toggleTheme', () => {});
await render(
@@ -37,21 +42,17 @@ module('Integration | Component | settings-card/application', function (hooks) {
});
test('it renders errors', async function (assert) {
- this.set('model', {
- cacheDaemonErrors: [
- {
- message: `this is a resolvable alias error`,
- name: 'resolvable-alias',
- },
- {
- message: `this is a target error`,
- name: 'target',
- },
- ],
- formattedDesktopVersion: 'v2.0.0',
- formattedCliVersion: 'v3.0.0',
- formattedCacheVersion: 'v4.0.0',
- });
+ model.cacheDaemonStatus.errors = [
+ {
+ message: `this is a resolvable alias error`,
+ name: 'resolvable-alias',
+ },
+ {
+ message: `this is a target error`,
+ name: 'target',
+ },
+ ];
+ this.set('model', model);
this.set('toggleTheme', () => {});
await render(
@@ -66,12 +67,7 @@ module('Integration | Component | settings-card/application', function (hooks) {
});
test('it indicates when cache daemon is running', async function (assert) {
- this.set('model', {
- cacheDaemonErrors: [],
- formattedDesktopVersion: 'v2.0.0',
- formattedCliVersion: 'v3.0.0',
- formattedCacheVersion: 'v4.0.0',
- });
+ this.set('model', model);
this.set('toggleTheme', () => {});
await render(
@@ -80,12 +76,8 @@ module('Integration | Component | settings-card/application', function (hooks) {
assert.dom(CACHE_DAEMON_VERSION).hasText('v4.0.0 Running');
- this.set('model', {
- cacheDaemonErrors: [],
- formattedDesktopVersion: 'v2.0.0',
- formattedCliVersion: 'v3.0.0',
- formattedCacheVersion: '',
- });
+ model.cacheDaemonStatus.version = '';
+ this.set('model', structuredClone(model));
assert.dom(CACHE_DAEMON_VERSION).hasText('Not Running');
});
});
diff --git a/ui/desktop/tests/integration/components/settings-card/client-agent-test.js b/ui/desktop/tests/integration/components/settings-card/client-agent-test.js
new file mode 100644
index 000000000..45e8ae6af
--- /dev/null
+++ b/ui/desktop/tests/integration/components/settings-card/client-agent-test.js
@@ -0,0 +1,84 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+import { setupIntl } from 'ember-intl/test-support';
+
+const STATUS_BADGE = '.hds-badge';
+const ACTION_BUTTON = 'button';
+const CARD_CONTAINER = 'hds-card__container';
+const ERROR_MESSAGES = '.hds-alert__description > p';
+const LAST_ERROR_MESSAGE = '.hds-alert__description > p:last-child';
+
+module(
+ 'Integration | Component | settings-card/client-agent',
+ function (hooks) {
+ setupRenderingTest(hooks);
+ setupIntl(hooks);
+
+ test('it renders running badge and pause button', async function (assert) {
+ this.set('model', {
+ clientAgentStatus: {
+ status: 'running',
+ errors: [],
+ },
+ });
+
+ await render(hbs``);
+
+ assert.dom(STATUS_BADGE).hasText('Running');
+ assert.dom(ACTION_BUTTON).hasText('Pause');
+ });
+
+ test('it renders paused badge and resume button', async function (assert) {
+ this.set('model', {
+ clientAgentStatus: {
+ status: 'paused',
+ errors: [],
+ },
+ });
+
+ await render(hbs``);
+
+ assert.dom(STATUS_BADGE).hasText('Paused');
+ assert.dom(ACTION_BUTTON).hasText('Resume');
+ });
+
+ test('it does not render component if there is no status', async function (assert) {
+ this.set('model', {
+ clientAgentStatus: {
+ errors: [{ message: 'agent is offline' }],
+ },
+ });
+
+ await render(hbs``);
+
+ assert.dom(STATUS_BADGE).doesNotExist();
+ assert.dom(ACTION_BUTTON).doesNotExist();
+ assert.dom(CARD_CONTAINER).doesNotExist();
+ });
+
+ test('it renders errors', async function (assert) {
+ this.set('model', {
+ clientAgentStatus: {
+ status: 'running',
+ errors: [
+ { message: 'agent is wonky' },
+ { message: 'another error' },
+ { message: 'big oof' },
+ ],
+ },
+ });
+
+ await render(hbs``);
+
+ assert.dom(ERROR_MESSAGES).isVisible({ count: 3 });
+ assert.dom(LAST_ERROR_MESSAGE).hasText('big oof');
+ });
+ },
+);