Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed9371fab7 | ||
|
|
68a01cfa3a | ||
|
|
b7129e38a8 | ||
|
|
26e27fd24b | ||
|
|
d0612c0a46 | ||
|
|
347107c109 | ||
|
|
0dac4ccf47 |
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 };
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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 @@
|
||||
0.21.2
|
||||
0.21.3
|
||||
Reference in new issue
Block a user