Compare commits

..
7 Commits
Author SHA1 Message Date
Zhihe Li ed9371fab7 chore: 🤖 Update CLI version to correct one (#3336) 2026-07-10 12:17:57 -04:00
Zhihe Li 68a01cfa3a chore: 🤖 Update CLI version (#3335) 2026-07-10 10:17:51 -04:00
Priya Patel b7129e38a8 fix: 🐛 credential library update (#3328)
* fix: 🐛 update credential library serializer

* test: 💍 adds acceptance test

* test: 💍 fix test

* fix: 🐛 fix test

* test: 💍 adds a11y ignore

* chore: 🤖 pr feedback
2026-07-08 14:38:48 -07:00
Lisbet Alvarez 26e27fd24b fix: 🐛 code mirror lint tooltip format issues (#3325)
* fix: 🐛 code mirror lint tooltip format issues

* refactor: 💡 add fallback for parent config

* refactor: 💡 use getOwner to retrieve actual app root element
2026-06-19 12:52:48 -07:00
Zhihe Li d0612c0a46 feat: 🎸 No-op linting errors should be warnings (#3326) 2026-06-19 15:15:58 -04:00
Zhihe Li 347107c109 feat: 🎸 Add discard modal for edit grants page (#3324) 2026-06-18 13:35:49 -04:00
Lisbet Alvarez 0dac4ccf47 Use codemirror tooltip extension to set parent to fix positioning bug (#3323)
* fix: 🐛 absolute positioning of cm tooltip in firefox

* docs: ✏️ add comment

* fix: 🐛 tooltip hidden in editor fullscreen mode & dark mode

* fix: 🐛 tooltip color in dark mode

* docs: ✏️ add comment
2026-06-18 09:59:38 -07:00
12 changed files with 261 additions and 47 deletions

No files matched your search

@@ -68,21 +68,25 @@ export default class CredentialLibrarySerializer extends ApplicationSerializer {
*/
handleCredentialMappingOverrides(serialized, isNew) {
const { credential_type, credential_mapping_overrides } = serialized;
if (Object.keys(credential_mapping_overrides).length === 0) {
serialized.credential_mapping_overrides = null;
}
// API expects to send null to fields if it is undefined or deleted
if (credential_mapping_overrides && !isNew) {
serialized.credential_mapping_overrides = options.mapping_overrides[
credential_type
].reduce((obj, key) => {
if (credential_mapping_overrides[key]) {
obj[key] = credential_mapping_overrides[key];
} else {
obj[key] = null;
}
return obj;
}, {});
if (!credential_mapping_overrides || isNew || !credential_type) {
return;
}
// API expects to send null to fields if it is undefined or deleted
serialized.credential_mapping_overrides = options.mapping_overrides[
credential_type
].reduce((obj, key) => {
if (credential_mapping_overrides[key]) {
obj[key] = credential_mapping_overrides[key];
} else {
obj[key] = null;
}
return obj;
}, {});
}
}
@@ -138,6 +138,45 @@ module('Unit | Serializer | credential library', function (hooks) {
});
});
test('it serializes credential library updates without credential_type', function (assert) {
const store = this.owner.lookup('service:store');
const serializer = store.serializerFor('credential-library');
store.push({
data: {
id: '1',
type: 'credential-library',
attributes: {
type: TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
name: 'Name',
description: 'Description',
path: '/vault/path',
http_method: 'GET',
version: 1,
credential_type: null,
credential_mapping_overrides: {},
},
},
});
const record = store.peekRecord('credential-library', '1');
const snapshot = record._createSnapshot();
const serializedRecord = serializer.serialize(snapshot);
assert.deepEqual(serializedRecord, {
type: TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
credential_store_id: null,
name: 'Name',
description: 'Description',
credential_type: null,
credential_mapping_overrides: null,
attributes: {
path: '/vault/path',
http_method: 'GET',
http_request_body: null,
},
version: 1,
});
});
test('it does not serialize http_request_body unless http_method is set to POST', function (assert) {
const store = this.owner.lookup('service:store');
const record = store.createRecord('credential-library', {
@@ -203,30 +203,30 @@
// Overrides for CodeMirror 6 autocomplete and lint tooltips/panels.
@mixin dark-mode-codemirror-override {
/* stylelint-disable selector-class-pattern */
// Autocomplete dropdown and lint hover tooltips
.cm-tooltip {
background-color: var(--token-color-palette-neutral-50);
border: 1px solid var(--token-color-palette-neutral-100);
color: var(--token-color-palette-neutral-400);
}
// Arrow pointer for tooltips shown above the cursor
.cm-tooltip-above::before {
border-top-color: var(--token-color-palette-neutral-50);
}
// Arrow pointer for tooltips shown below the cursor
.cm-tooltip-below::before {
border-bottom-color: var(--token-color-palette-neutral-50);
}
// Autocomplete list > selected item highlight
.cm-tooltip-autocomplete > ul > li[aria-selected] {
background-color: var(--token-color-palette-blue-300);
color: var(--token-color-palette-neutral-700);
}
.cm-editor {
// Autocomplete dropdown and lint hover tooltips
.cm-tooltip {
background-color: var(--token-color-palette-neutral-50);
border: 1px solid var(--token-color-palette-neutral-100);
color: var(--token-color-palette-neutral-400);
}
// Arrow pointer for tooltips shown above the cursor
.cm-tooltip-above::before {
border-top-color: var(--token-color-palette-neutral-50);
}
// Arrow pointer for tooltips shown below the cursor
.cm-tooltip-below::before {
border-bottom-color: var(--token-color-palette-neutral-50);
}
// Autocomplete list > selected item highlight
.cm-tooltip-autocomplete > ul > li[aria-selected] {
background-color: var(--token-color-palette-blue-300);
color: var(--token-color-palette-neutral-700);
}
// Content/gutter border
.cm-content {
border-left: 1px solid var(--token-color-palette-neutral-100);
@@ -44,7 +44,7 @@
@color='secondary'
{{on 'click' @cancel}}
/>
{{#if this.grantStringsText}}
{{#if this.grantStrings.length}}
<Hds::Button
@text={{t 'actions.export'}}
@color='tertiary'
@@ -7,7 +7,9 @@ import Component from '@glimmer/component';
import { action } from '@ember/object';
import { service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { getOwner } from '@ember/application';
import {
tooltips,
autocompletion,
completionKeymap,
startCompletion,
@@ -36,7 +38,6 @@ export default class FormRoleEditGrantsComponent extends Component {
exportOptionsMap = { terraform: 'terraform', nativeHCL: 'native-hcl' };
exportOptions = Object.values(this.exportOptionsMap);
@tracked grantStringsText = (this.args.model?.grant_strings ?? []).join('\n');
@tracked currentLineText = this.args.model?.grant_strings?.[0] ?? '';
@tracked showExportOptionsFlyout = false;
@tracked selectedExportOption = this.exportOptions[0];
@@ -168,7 +169,14 @@ export default class FormRoleEditGrantsComponent extends Component {
this.translateLintingError,
);
rootElementSelector = getOwner(this).rootElement;
rootElement = getOwner(this)
.lookup('service:-document')
.querySelector(this.rootElementSelector);
customExtensions = [
// Configure tooltips to render in app root to avoid overflow issues within the editor container.
tooltips({ parent: this.rootElement }),
autocompletion({
override: [this.completionSource],
// Trigger autocompletion when the user completes a grant field (which we labeled as keywords)
@@ -179,7 +187,11 @@ export default class FormRoleEditGrantsComponent extends Component {
const line = update.state.doc.lineAt(update.state.selection.main.head);
this.currentLineText = line.text;
if (update.docChanged) {
this.grantStringsText = update.state.doc.toString();
const text = update.state.doc.toString();
this.args.model.grant_strings = text
.split('\n')
.map((s) => s.trim())
.filter(Boolean);
}
// Trigger autocomplete when we're on an empty line
if (line.text === '') {
@@ -197,11 +209,12 @@ export default class FormRoleEditGrantsComponent extends Component {
this.#editorView?.destroy();
}
get grantStringsText() {
return this.grantStrings.join('\n');
}
get grantStrings() {
return this.grantStringsText
.split('\n')
.map((grantString) => grantString.trim())
.filter(Boolean);
return this.args.model?.grant_strings ?? [];
}
/**
@@ -221,7 +234,7 @@ export default class FormRoleEditGrantsComponent extends Component {
*/
get terraformFormattedExport() {
let formatted = `grant_strings = [ \n`;
this.grantStringsText.split('\n').forEach((line) => {
this.grantStrings.forEach((line) => {
formatted += ` "${line}",\n`;
});
formatted += `]\n`;
@@ -234,7 +247,7 @@ export default class FormRoleEditGrantsComponent extends Component {
*/
get nativeHclFormattedExport() {
let formatted = `[ \n`;
this.grantStringsText.split('\n').forEach((line) => {
this.grantStrings.forEach((line) => {
formatted += ` "${line}",\n`;
});
formatted += `]\n`;
@@ -4,6 +4,7 @@
*/
import Route from '@ember/routing/route';
import { action } from '@ember/object';
import { service } from '@ember/service';
import { dropTask } from 'ember-concurrency';
import { modelMapping } from 'api/services/sqlite';
@@ -60,4 +61,15 @@ export default class ScopesScopeRolesRoleEditGrantsRoute extends Route {
return response.json();
}
/**
* Stores the role model in the transition data property so that the application level hook
* can check for dirty attributes and trigger the confirm service.
* @param {object} transition
*/
@action
async willTransition(transition) {
const { role } = transition.from.attributes;
transition.data = { model: role };
}
}
+6
View File
@@ -1042,3 +1042,9 @@
max-height: 50vh;
overflow-y: auto;
}
// Prevent grants editor tooltip from being
// hidden by code editor in fullscreen mode.
.ember-application div .cm-tooltip {
z-index: 1000;
}
+2 -2
View File
@@ -617,7 +617,7 @@ const validateActionsField = (
diagnostics.push({
from: pos.valueStart,
to: pos.valueEnd,
severity: 'error',
severity: 'warning',
message: translate('actions.noop-requires-list'),
});
return;
@@ -637,7 +637,7 @@ const validateActionsField = (
diagnostics.push({
from: pos.valueStart,
to: pos.valueEnd,
severity: 'error',
severity: 'warning',
message: translate('actions.list-requires-noop'),
actions: [createAddTextAction(translate('editor.add-noop'))],
});
@@ -460,4 +460,58 @@ module('Acceptance | credential-libraries | update', function (hooks) {
selectors.FIELD_VAULT_PATH_VALUE,
);
});
test('can update a vault generic credential library without credential_type and save changes without error', async function (assert) {
setRunOptions({
rules: {
'color-contrast': {
// [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2026-07-07
enabled: false,
},
},
});
const credentialLibraryWithoutType = this.server.create(
'credential-library',
{
scope: instances.scopes.project,
credentialStore: instances.credentialStore,
type: TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
credential_type: null,
credentialMappingOverrides: null,
},
);
await visit(
`${urls.credentialLibraries}/${credentialLibraryWithoutType.id}`,
);
await click(commonSelectors.EDIT_BTN, 'Activate edit mode');
await fillIn(commonSelectors.FIELD_NAME, commonSelectors.FIELD_NAME_VALUE);
await fillIn(
commonSelectors.FIELD_DESCRIPTION,
commonSelectors.FIELD_DESCRIPTION_VALUE,
);
await fillIn(selectors.FIELD_VAULT_PATH, selectors.FIELD_VAULT_PATH_VALUE);
await click(commonSelectors.SAVE_BTN);
const credentialLibrary = this.server.schema.credentialLibraries.find(
credentialLibraryWithoutType.id,
);
assert.strictEqual(
credentialLibrary.name,
commonSelectors.FIELD_NAME_VALUE,
);
assert.strictEqual(
credentialLibrary.description,
commonSelectors.FIELD_DESCRIPTION_VALUE,
);
assert.strictEqual(
credentialLibrary.attributes.path,
selectors.FIELD_VAULT_PATH_VALUE,
);
assert.strictEqual(credentialLibrary.credentialType, null);
});
});
@@ -348,4 +348,83 @@ module('Acceptance | roles/edit grants', function (hooks) {
assert.dom(selectors.AUTOCOMPLETE_TOOLTIP).isVisible();
});
test('can discard unsaved grant changes via dialog', async function (assert) {
setRunOptions({
rules: {
'color-contrast': {
// [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2026-06-17
enabled: false,
},
},
});
assert.expect(3);
const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true;
await visit(urls.editGrants);
await waitFor(commonSelectors.CODE_EDITOR_CM);
const newGrantString = 'ids=*;type=session;actions=list';
const editorElement = find(commonSelectors.CODE_EDITOR_CODE);
const editorView = editorElement.editor;
editorView.dispatch({
changes: {
from: editorView.state.doc.length,
insert: `\n${newGrantString}`,
},
});
await click(commonSelectors.HREF(urls.role));
assert.dom(commonSelectors.MODAL_WARNING).isVisible();
await click(commonSelectors.MODAL_WARNING_CONFIRM_BTN);
assert.strictEqual(currentURL(), urls.role);
console.log(this.server.schema.roles.find(instances.role.id));
assert.false(
this.server.schema.roles
.find(instances.role.id)
.grant_strings.includes(newGrantString),
);
});
test('can cancel discard grant changes via dialog', async function (assert) {
setRunOptions({
rules: {
'color-contrast': {
// [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2026-06-17
enabled: false,
},
},
});
assert.expect(3);
const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true;
await visit(urls.editGrants);
await waitFor(commonSelectors.CODE_EDITOR_CM);
const newGrantString = 'ids=*;type=session;actions=list';
const editorElement = find(commonSelectors.CODE_EDITOR_CODE);
const editorView = editorElement.editor;
editorView.dispatch({
changes: {
from: editorView.state.doc.length,
insert: `\n${newGrantString}`,
},
});
await click(commonSelectors.HREF(urls.role));
assert.dom(commonSelectors.MODAL_WARNING).isVisible();
await click(commonSelectors.MODAL_WARNING_CANCEL_BTN);
assert.strictEqual(currentURL(), urls.editGrants);
assert
.dom(commonSelectors.CODE_EDITOR_CONTENT)
.includesText(newGrantString);
});
});
+7
View File
@@ -2,6 +2,13 @@
Canonical reference for changes, improvements, and bugfixes for Boundary Desktop.
## v2.6.2 [CLI 0.21.3] (2026.07.10)
### New & Improved
- Upgrade CLI to v0.21.3 ([PR](https://github.com/hashicorp/boundary-ui/pull/3336))
## V2.6.1 [CLI 0.21.2] (2026.06.30)
### Bug Fixes
+1 -1
View File
@@ -1 +1 @@
0.21.2
0.21.3