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) {
|
handleCredentialMappingOverrides(serialized, isNew) {
|
||||||
const { credential_type, credential_mapping_overrides } = serialized;
|
const { credential_type, credential_mapping_overrides } = serialized;
|
||||||
|
|
||||||
if (Object.keys(credential_mapping_overrides).length === 0) {
|
if (Object.keys(credential_mapping_overrides).length === 0) {
|
||||||
serialized.credential_mapping_overrides = null;
|
serialized.credential_mapping_overrides = null;
|
||||||
}
|
}
|
||||||
// API expects to send null to fields if it is undefined or deleted
|
|
||||||
if (credential_mapping_overrides && !isNew) {
|
if (!credential_mapping_overrides || isNew || !credential_type) {
|
||||||
serialized.credential_mapping_overrides = options.mapping_overrides[
|
return;
|
||||||
credential_type
|
|
||||||
].reduce((obj, key) => {
|
|
||||||
if (credential_mapping_overrides[key]) {
|
|
||||||
obj[key] = credential_mapping_overrides[key];
|
|
||||||
} else {
|
|
||||||
obj[key] = null;
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
}, {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
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 store = this.owner.lookup('service:store');
|
||||||
const record = store.createRecord('credential-library', {
|
const record = store.createRecord('credential-library', {
|
||||||
|
|||||||
@@ -203,30 +203,30 @@
|
|||||||
// Overrides for CodeMirror 6 autocomplete and lint tooltips/panels.
|
// Overrides for CodeMirror 6 autocomplete and lint tooltips/panels.
|
||||||
@mixin dark-mode-codemirror-override {
|
@mixin dark-mode-codemirror-override {
|
||||||
/* stylelint-disable selector-class-pattern */
|
/* 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 {
|
.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
|
// Content/gutter border
|
||||||
.cm-content {
|
.cm-content {
|
||||||
border-left: 1px solid var(--token-color-palette-neutral-100);
|
border-left: 1px solid var(--token-color-palette-neutral-100);
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
@color='secondary'
|
@color='secondary'
|
||||||
{{on 'click' @cancel}}
|
{{on 'click' @cancel}}
|
||||||
/>
|
/>
|
||||||
{{#if this.grantStringsText}}
|
{{#if this.grantStrings.length}}
|
||||||
<Hds::Button
|
<Hds::Button
|
||||||
@text={{t 'actions.export'}}
|
@text={{t 'actions.export'}}
|
||||||
@color='tertiary'
|
@color='tertiary'
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import Component from '@glimmer/component';
|
|||||||
import { action } from '@ember/object';
|
import { action } from '@ember/object';
|
||||||
import { service } from '@ember/service';
|
import { service } from '@ember/service';
|
||||||
import { tracked } from '@glimmer/tracking';
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
import { getOwner } from '@ember/application';
|
||||||
import {
|
import {
|
||||||
|
tooltips,
|
||||||
autocompletion,
|
autocompletion,
|
||||||
completionKeymap,
|
completionKeymap,
|
||||||
startCompletion,
|
startCompletion,
|
||||||
@@ -36,7 +38,6 @@ export default class FormRoleEditGrantsComponent extends Component {
|
|||||||
exportOptionsMap = { terraform: 'terraform', nativeHCL: 'native-hcl' };
|
exportOptionsMap = { terraform: 'terraform', nativeHCL: 'native-hcl' };
|
||||||
exportOptions = Object.values(this.exportOptionsMap);
|
exportOptions = Object.values(this.exportOptionsMap);
|
||||||
|
|
||||||
@tracked grantStringsText = (this.args.model?.grant_strings ?? []).join('\n');
|
|
||||||
@tracked currentLineText = this.args.model?.grant_strings?.[0] ?? '';
|
@tracked currentLineText = this.args.model?.grant_strings?.[0] ?? '';
|
||||||
@tracked showExportOptionsFlyout = false;
|
@tracked showExportOptionsFlyout = false;
|
||||||
@tracked selectedExportOption = this.exportOptions[0];
|
@tracked selectedExportOption = this.exportOptions[0];
|
||||||
@@ -168,7 +169,14 @@ export default class FormRoleEditGrantsComponent extends Component {
|
|||||||
this.translateLintingError,
|
this.translateLintingError,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
rootElementSelector = getOwner(this).rootElement;
|
||||||
|
rootElement = getOwner(this)
|
||||||
|
.lookup('service:-document')
|
||||||
|
.querySelector(this.rootElementSelector);
|
||||||
|
|
||||||
customExtensions = [
|
customExtensions = [
|
||||||
|
// Configure tooltips to render in app root to avoid overflow issues within the editor container.
|
||||||
|
tooltips({ parent: this.rootElement }),
|
||||||
autocompletion({
|
autocompletion({
|
||||||
override: [this.completionSource],
|
override: [this.completionSource],
|
||||||
// Trigger autocompletion when the user completes a grant field (which we labeled as keywords)
|
// 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);
|
const line = update.state.doc.lineAt(update.state.selection.main.head);
|
||||||
this.currentLineText = line.text;
|
this.currentLineText = line.text;
|
||||||
if (update.docChanged) {
|
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
|
// Trigger autocomplete when we're on an empty line
|
||||||
if (line.text === '') {
|
if (line.text === '') {
|
||||||
@@ -197,11 +209,12 @@ export default class FormRoleEditGrantsComponent extends Component {
|
|||||||
this.#editorView?.destroy();
|
this.#editorView?.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get grantStringsText() {
|
||||||
|
return this.grantStrings.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
get grantStrings() {
|
get grantStrings() {
|
||||||
return this.grantStringsText
|
return this.args.model?.grant_strings ?? [];
|
||||||
.split('\n')
|
|
||||||
.map((grantString) => grantString.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -221,7 +234,7 @@ export default class FormRoleEditGrantsComponent extends Component {
|
|||||||
*/
|
*/
|
||||||
get terraformFormattedExport() {
|
get terraformFormattedExport() {
|
||||||
let formatted = `grant_strings = [ \n`;
|
let formatted = `grant_strings = [ \n`;
|
||||||
this.grantStringsText.split('\n').forEach((line) => {
|
this.grantStrings.forEach((line) => {
|
||||||
formatted += ` "${line}",\n`;
|
formatted += ` "${line}",\n`;
|
||||||
});
|
});
|
||||||
formatted += `]\n`;
|
formatted += `]\n`;
|
||||||
@@ -234,7 +247,7 @@ export default class FormRoleEditGrantsComponent extends Component {
|
|||||||
*/
|
*/
|
||||||
get nativeHclFormattedExport() {
|
get nativeHclFormattedExport() {
|
||||||
let formatted = `[ \n`;
|
let formatted = `[ \n`;
|
||||||
this.grantStringsText.split('\n').forEach((line) => {
|
this.grantStrings.forEach((line) => {
|
||||||
formatted += ` "${line}",\n`;
|
formatted += ` "${line}",\n`;
|
||||||
});
|
});
|
||||||
formatted += `]\n`;
|
formatted += `]\n`;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Route from '@ember/routing/route';
|
import Route from '@ember/routing/route';
|
||||||
|
import { action } from '@ember/object';
|
||||||
import { service } from '@ember/service';
|
import { service } from '@ember/service';
|
||||||
import { dropTask } from 'ember-concurrency';
|
import { dropTask } from 'ember-concurrency';
|
||||||
import { modelMapping } from 'api/services/sqlite';
|
import { modelMapping } from 'api/services/sqlite';
|
||||||
@@ -60,4 +61,15 @@ export default class ScopesScopeRolesRoleEditGrantsRoute extends Route {
|
|||||||
|
|
||||||
return response.json();
|
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;
|
max-height: 50vh;
|
||||||
overflow-y: auto;
|
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({
|
diagnostics.push({
|
||||||
from: pos.valueStart,
|
from: pos.valueStart,
|
||||||
to: pos.valueEnd,
|
to: pos.valueEnd,
|
||||||
severity: 'error',
|
severity: 'warning',
|
||||||
message: translate('actions.noop-requires-list'),
|
message: translate('actions.noop-requires-list'),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -637,7 +637,7 @@ const validateActionsField = (
|
|||||||
diagnostics.push({
|
diagnostics.push({
|
||||||
from: pos.valueStart,
|
from: pos.valueStart,
|
||||||
to: pos.valueEnd,
|
to: pos.valueEnd,
|
||||||
severity: 'error',
|
severity: 'warning',
|
||||||
message: translate('actions.list-requires-noop'),
|
message: translate('actions.list-requires-noop'),
|
||||||
actions: [createAddTextAction(translate('editor.add-noop'))],
|
actions: [createAddTextAction(translate('editor.add-noop'))],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -460,4 +460,58 @@ module('Acceptance | credential-libraries | update', function (hooks) {
|
|||||||
selectors.FIELD_VAULT_PATH_VALUE,
|
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();
|
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.
|
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)
|
## V2.6.1 [CLI 0.21.2] (2026.06.30)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
0.21.2
|
0.21.3
|
||||||
Reference in new issue
Block a user