diff --git a/ui/packages/consul-ui/app/components/consul/peer/address/list/index.hbs b/ui/packages/consul-ui/app/components/consul/peer/address/list/index.hbs
index aabada41ad1..a9a90b2e09a 100644
--- a/ui/packages/consul-ui/app/components/consul/peer/address/list/index.hbs
+++ b/ui/packages/consul-ui/app/components/consul/peer/address/list/index.hbs
@@ -3,28 +3,29 @@
SPDX-License-Identifier: BUSL-1.1
}}
-
- {{#if p.data.height}}
-
- {{/if}}
-
\ No newline at end of file
+
+
+
+
+
diff --git a/ui/packages/consul-ui/app/components/consul/peer/address/list/index.js b/ui/packages/consul-ui/app/components/consul/peer/address/list/index.js
new file mode 100644
index 00000000000..9f51aa8d1f8
--- /dev/null
+++ b/ui/packages/consul-ui/app/components/consul/peer/address/list/index.js
@@ -0,0 +1,21 @@
+/**
+ * Copyright IBM Corp. 2024, 2026
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Component from '@glimmer/component';
+
+// Single column, not sortable (per Figma — no sort icon on "Address"; server
+// addresses have no inherent order worth exposing).
+const COLUMNS = [{ label: 'Address', width: '100%' }];
+
+/**
+ * Consul::Peer::Address::List
+ *
+ * Server-addresses-tab specific configuration for the generic
+ * Consul::DataTable. Each row is the plain address string plus a copy-to-
+ * clipboard action; there is no per-row navigation.
+ */
+export default class ConsulPeerAddressList extends Component {
+ columns = COLUMNS;
+}
diff --git a/ui/packages/consul-ui/app/components/consul/peer/address/list/index.scss b/ui/packages/consul-ui/app/components/consul/peer/address/list/index.scss
index 18935e6b7c2..3dae812910b 100644
--- a/ui/packages/consul-ui/app/components/consul/peer/address/list/index.scss
+++ b/ui/packages/consul-ui/app/components/consul/peer/address/list/index.scss
@@ -3,6 +3,14 @@
* SPDX-License-Identifier: BUSL-1.1
*/
-.border-bottom-primary {
- border-bottom: 1px solid var(--token-color-border-primary);
+/* Gap between the tab nav and the table card — matches Figma (24px) and
+ Consul::Peer::Exported::List's equivalent rule. */
+.consul-peer-address-list {
+ margin-top: 24px;
+}
+
+.consul-peer-address-list__address {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
}
diff --git a/ui/packages/consul-ui/app/components/consul/peer/address/toolbar/index.hbs b/ui/packages/consul-ui/app/components/consul/peer/address/toolbar/index.hbs
new file mode 100644
index 00000000000..f621390a884
--- /dev/null
+++ b/ui/packages/consul-ui/app/components/consul/peer/address/toolbar/index.hbs
@@ -0,0 +1,15 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: BUSL-1.1
+}}
+
+{{! Server-addresses-tab toolbar: search only, per Figma — no filter groups
+ and no sort control (the "Address" column isn't sortable, see
+ Consul::Peer::Address::List). }}
+
diff --git a/ui/packages/consul-ui/app/components/consul/peer/address/toolbar/index.scss b/ui/packages/consul-ui/app/components/consul/peer/address/toolbar/index.scss
new file mode 100644
index 00000000000..ffc173ee036
--- /dev/null
+++ b/ui/packages/consul-ui/app/components/consul/peer/address/toolbar/index.scss
@@ -0,0 +1,23 @@
+/**
+ * Copyright IBM Corp. 2024, 2026
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+/* The toolbar renders inside .consul-data-table__card, above the table (see
+ Consul::Peer::Address::List). Give it the same faint-surface background,
+ border and squared bottom corners as Consul::Peer::Toolbar, so the whole
+ toolbar+table reads as one card. */
+.consul-peer-address-toolbar {
+ padding: 8px;
+ background-color: var(--token-color-surface-faint);
+ border: 1px solid var(--token-color-border-faint);
+ border-bottom: none;
+ border-radius: 6px 6px 0 0;
+}
+
+/* Separator between the controls row (Search) and the applied-filters area
+ ("No filters applied"). */
+.consul-peer-address-toolbar .hds-filter-bar__actions {
+ border-bottom: 1px solid var(--token-color-border-faint);
+ padding-bottom: 8px;
+}
diff --git a/ui/packages/consul-ui/app/components/consul/peer/exported/list/index.hbs b/ui/packages/consul-ui/app/components/consul/peer/exported/list/index.hbs
new file mode 100644
index 00000000000..652204b83d3
--- /dev/null
+++ b/ui/packages/consul-ui/app/components/consul/peer/exported/list/index.hbs
@@ -0,0 +1,35 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: BUSL-1.1
+}}
+
+
+
+ {{! Passed straight through from the caller so the toolbar renders inside
+ .consul-data-table__card, matching Consul::Peer::List's pattern. }}
+ <:toolbar>
+ {{yield to="toolbar"}}
+
+ <:row as |item B|>
+
+ {{item.Name}}
+
+
+
+
diff --git a/ui/packages/consul-ui/app/components/consul/peer/exported/list/index.js b/ui/packages/consul-ui/app/components/consul/peer/exported/list/index.js
new file mode 100644
index 00000000000..fbee840fb58
--- /dev/null
+++ b/ui/packages/consul-ui/app/components/consul/peer/exported/list/index.js
@@ -0,0 +1,29 @@
+/**
+ * Copyright IBM Corp. 2024, 2026
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Component from '@glimmer/component';
+
+// Single sortable column, matching Figma (search-only toolbar plus
+// click-to-sort on the "Service name" header; no separate quick-sort dropdown
+// or per-row actions).
+const COLUMNS = [
+ {
+ label: 'Service name',
+ sortKey: 'Name',
+ sortValue: (item) => (item.Name || '').toLowerCase(),
+ width: '100%',
+ },
+];
+
+/**
+ * Consul::Peer::Exported::List
+ *
+ * Peer-exported-services-tab specific configuration for the generic
+ * Consul::DataTable. Rows link through to the service's show page (in the
+ * peer's own partition/namespace when set); there are no per-row actions.
+ */
+export default class ConsulPeerExportedList extends Component {
+ columns = COLUMNS;
+}
diff --git a/ui/packages/consul-ui/app/components/consul/peer/exported/list/index.scss b/ui/packages/consul-ui/app/components/consul/peer/exported/list/index.scss
new file mode 100644
index 00000000000..8e991ad4c90
--- /dev/null
+++ b/ui/packages/consul-ui/app/components/consul/peer/exported/list/index.scss
@@ -0,0 +1,9 @@
+/**
+ * Copyright IBM Corp. 2024, 2026
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+/* Gap between the tab nav and the table card — matches Figma (24px). */
+.consul-peer-exported-list {
+ margin-top: 24px;
+}
diff --git a/ui/packages/consul-ui/app/components/consul/peer/exported/toolbar/index.hbs b/ui/packages/consul-ui/app/components/consul/peer/exported/toolbar/index.hbs
new file mode 100644
index 00000000000..9b0004562d9
--- /dev/null
+++ b/ui/packages/consul-ui/app/components/consul/peer/exported/toolbar/index.hbs
@@ -0,0 +1,15 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: BUSL-1.1
+}}
+
+{{! Exported-services-tab toolbar: search only, per Figma — no filter groups
+ and no quick-sort dropdown (sorting is the "Service name" column's own
+ click-to-sort, see Consul::Peer::Exported::List). }}
+
diff --git a/ui/packages/consul-ui/app/components/consul/peer/exported/toolbar/index.scss b/ui/packages/consul-ui/app/components/consul/peer/exported/toolbar/index.scss
new file mode 100644
index 00000000000..0db3c6c44c9
--- /dev/null
+++ b/ui/packages/consul-ui/app/components/consul/peer/exported/toolbar/index.scss
@@ -0,0 +1,23 @@
+/**
+ * Copyright IBM Corp. 2024, 2026
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+/* The toolbar renders inside .consul-data-table__card, above the table (see
+ Consul::Peer::Exported::List). Give it the same faint-surface background,
+ border and squared bottom corners as Consul::Peer::Toolbar, so the whole
+ toolbar+table reads as one card. */
+.consul-peer-exported-toolbar {
+ padding: 8px;
+ background-color: var(--token-color-surface-faint);
+ border: 1px solid var(--token-color-border-faint);
+ border-bottom: none;
+ border-radius: 6px 6px 0 0;
+}
+
+/* Separator between the controls row (Search) and the applied-filters area
+ ("No filters applied"). */
+.consul-peer-exported-toolbar .hds-filter-bar__actions {
+ border-bottom: 1px solid var(--token-color-border-faint);
+ padding-bottom: 8px;
+}
diff --git a/ui/packages/consul-ui/app/components/consul/peer/form/cancel/index.hbs b/ui/packages/consul-ui/app/components/consul/peer/form/cancel/index.hbs
new file mode 100644
index 00000000000..41888ae1e03
--- /dev/null
+++ b/ui/packages/consul-ui/app/components/consul/peer/form/cancel/index.hbs
@@ -0,0 +1,43 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: BUSL-1.1
+}}
+
+
+
+{{#if this.confirming}}
+
+ Discard unsaved changes?
+
+ You'll lose the information you've entered on this page.
+
+
+
+
+
+
+
+
+{{/if}}
diff --git a/ui/packages/consul-ui/app/components/consul/peer/form/cancel/index.js b/ui/packages/consul-ui/app/components/consul/peer/form/cancel/index.js
new file mode 100644
index 00000000000..0c57f2e7575
--- /dev/null
+++ b/ui/packages/consul-ui/app/components/consul/peer/form/cancel/index.js
@@ -0,0 +1,40 @@
+/**
+ * Copyright IBM Corp. 2024, 2026
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { action } from '@ember/object';
+
+// Cancel button shared by the peer create form's tabs. When the form has
+// unsaved input, clicking it asks for confirmation before discarding it and
+// invoking `@onconfirm` (navigating back to the peers list); an empty form
+// can be left without asking.
+export default class ConsulPeerFormCancelComponent extends Component {
+ @tracked confirming = false;
+
+ get dirty() {
+ return Boolean(this.args.dirty);
+ }
+
+ @action
+ click() {
+ if (this.dirty) {
+ this.confirming = true;
+ } else {
+ this.args.onconfirm();
+ }
+ }
+
+ @action
+ confirm() {
+ this.confirming = false;
+ this.args.onconfirm();
+ }
+
+ @action
+ keepEditing() {
+ this.confirming = false;
+ }
+}
diff --git a/ui/packages/consul-ui/app/components/consul/peer/form/generate/actions/index.hbs b/ui/packages/consul-ui/app/components/consul/peer/form/generate/actions/index.hbs
index 1c16d1d533d..16a3f6c47eb 100644
--- a/ui/packages/consul-ui/app/components/consul/peer/form/generate/actions/index.hbs
+++ b/ui/packages/consul-ui/app/components/consul/peer/form/generate/actions/index.hbs
@@ -3,10 +3,15 @@
SPDX-License-Identifier: BUSL-1.1
}}
-
\ No newline at end of file
+
+
+
+
\ No newline at end of file
diff --git a/ui/packages/consul-ui/app/components/consul/peer/form/generate/chart.xstate.js b/ui/packages/consul-ui/app/components/consul/peer/form/generate/chart.xstate.js
index 646a3421d59..e8fe134bf8b 100644
--- a/ui/packages/consul-ui/app/components/consul/peer/form/generate/chart.xstate.js
+++ b/ui/packages/consul-ui/app/components/consul/peer/form/generate/chart.xstate.js
@@ -31,6 +31,12 @@ export default {
},
},
},
- error: {},
+ error: {
+ on: {
+ LOAD: {
+ target: 'loading',
+ },
+ },
+ },
},
};
diff --git a/ui/packages/consul-ui/app/components/consul/peer/form/generate/fieldsets/index.hbs b/ui/packages/consul-ui/app/components/consul/peer/form/generate/fieldsets/index.hbs
index d17bc34a727..a780b4b14dc 100644
--- a/ui/packages/consul-ui/app/components/consul/peer/form/generate/fieldsets/index.hbs
+++ b/ui/packages/consul-ui/app/components/consul/peer/form/generate/fieldsets/index.hbs
@@ -27,14 +27,22 @@
as |Name|
}}
@@ -45,14 +53,18 @@
as |ServerExternalAddresses|
}}
diff --git a/ui/packages/consul-ui/app/components/consul/peer/form/generate/index.hbs b/ui/packages/consul-ui/app/components/consul/peer/form/generate/index.hbs
index cc5888372d5..584612337b3 100644
--- a/ui/packages/consul-ui/app/components/consul/peer/form/generate/index.hbs
+++ b/ui/packages/consul-ui/app/components/consul/peer/form/generate/index.hbs
@@ -16,24 +16,47 @@
{{#let (dom-guid) as |id reset|}}
{{/let}}
diff --git a/ui/packages/consul-ui/app/components/consul/peer/form/index.hbs b/ui/packages/consul-ui/app/components/consul/peer/form/index.hbs
index 031ffead3cc..6cea8ac0638 100644
--- a/ui/packages/consul-ui/app/components/consul/peer/form/index.hbs
+++ b/ui/packages/consul-ui/app/components/consul/peer/form/index.hbs
@@ -9,21 +9,29 @@
as |fsm|
>
-
+ {{! Tab order (Generate token, Establish peering) must match
+ consul/peer/form/index.js's TAB_EVENTS. Hds::Tabs requires one Panel
+ per Tab, but its panels stay mounted (just `hidden`) rather than
+ being torn down on switch, which would leave both tabs' same-named
+ form fields in the DOM at once. So Hds::Tabs only drives the tab
+ buttons here (its Panels are left empty) — the actual tab content
+ below is still gated by fsm.State, exactly as before, so only the
+ active tab is ever mounted. }}
+
+
+ {{t "components.consul.peer.form.generate-label"}}
+
+
+ {{t "components.consul.peer.form.establish-label"}}
+
+
+
+
+
@@ -32,7 +40,11 @@
as |source|
>
{{yield
- (hash Form=(component "consul/peer/form/generate" item=source.data))
+ (hash
+ Form=(component
+ "consul/peer/form/generate" item=source.data onCancel=@onCancel
+ )
+ )
}}
@@ -45,7 +57,14 @@
as |source|
>
{{yield
- (hash Form=(component "consul/peer/form/initiate" item=source.data))
+ (hash
+ Form=(component
+ "consul/peer/form/initiate"
+ item=source.data
+ onCancel=@onCancel
+ onsubmit=@onSubmit
+ )
+ )
}}
diff --git a/ui/packages/consul-ui/app/components/consul/peer/form/index.js b/ui/packages/consul-ui/app/components/consul/peer/form/index.js
new file mode 100644
index 00000000000..8a16068c227
--- /dev/null
+++ b/ui/packages/consul-ui/app/components/consul/peer/form/index.js
@@ -0,0 +1,17 @@
+/**
+ * Copyright IBM Corp. 2024, 2026
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+
+// Order matches the Hds::Tabs::Tab order rendered in the template below.
+const TAB_EVENTS = ['GENERATE', 'INITIATE'];
+
+export default class ConsulPeerFormComponent extends Component {
+ @action
+ onClickTab(dispatch, event, tabIndex) {
+ dispatch(TAB_EVENTS[tabIndex]);
+ }
+}
diff --git a/ui/packages/consul-ui/app/components/consul/peer/form/index.scss b/ui/packages/consul-ui/app/components/consul/peer/form/index.scss
index df095b98bb5..ef918d7017d 100644
--- a/ui/packages/consul-ui/app/components/consul/peer/form/index.scss
+++ b/ui/packages/consul-ui/app/components/consul/peer/form/index.scss
@@ -5,16 +5,69 @@
.consul-peer-form {
& {
- width: 416px;
+ /* Fills the page's full content column, instead of the old 416px modal
+ width this form used before it moved out of a dialog. */
+ width: 100%;
+ /* Tabs -> form content gap, per Figma. */
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
}
nav {
margin-bottom: 20px;
}
}
+.consul-peer-form-generate,
+.consul-peer-form-initiate {
+ /* Fields -> footer (Cancel/submit) gap, per Figma's Stepper::List ->
+ Footer spacing. The error alert (when present) shares this gap too. */
+ form {
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+ }
+}
+
+.consul-peer-form-generate-fieldsets {
+ /* Name / Server address(es) are two separate