Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions addons/core/translations/form/en-us.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,8 @@ other_fields:
help: This information is provided by the server and cannot be edited in Boundary
worker_filter:
label: Worker Filter
variable-time-field:
days: Days
hours: Hours
minutes: Minutes
set-to-max: Set to Max
2 changes: 1 addition & 1 deletion addons/rose/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
},
"dependencies": {
"@babel/core": "^7.26.10",
"@hashicorp/design-system-components": "^4.20.2",
"@hashicorp/design-system-components": "^4.24.1",
"@hashicorp/design-system-tokens": "^2.3.0",
"@hashicorp/flight-icons": "^3.10.0",
"@nullvoxpopuli/ember-composable-helpers": "^5.2.10",
Expand Down
143 changes: 100 additions & 43 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

53 changes: 53 additions & 0 deletions ui/admin/app/components/variable-time-field/index.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
}}
<Hds::Form::KeyValueInputs @isRequired={{@isRequired}} @data={{this.data}}>
<:header as |H|>
<H.Legend>{{@legend}}</H.Legend>
</:header>
<:row as |R|>
<R.Field as |F|>
<F.Label>
{{t 'form.variable-time-field.days'}}
</F.Label>
<F.TextInput
type='number'
name='days'
@value={{R.rowData.days}}
{{on 'input' (fn this.updateTime 'days')}}
/>
</R.Field>
<R.Field as |F|>
<F.Label>
{{t 'form.variable-time-field.hours'}}
</F.Label>
<F.TextInput
type='number'
name='hours'
@value={{R.rowData.hours}}
{{on 'input' (fn this.updateTime 'hours')}}
/>
</R.Field>
<R.Field as |F|>
<F.Label>
{{t 'form.variable-time-field.minutes'}}
</F.Label>
<F.TextInput
type='number'
name='minutes'
@value={{R.rowData.minutes}}
{{on 'input' (fn this.updateTime 'minutes')}}
/>
</R.Field>
{{#if @max}}
<R.Generic>
<Hds::Button
@text={{t 'form.variable-time-field.set-to-max'}}
variant='secondary'
{{on 'click' this.setMax}}
/>
</R.Generic>
{{/if}}
</:row>
</Hds::Form::KeyValueInputs>
62 changes: 62 additions & 0 deletions ui/admin/app/components/variable-time-field/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/

import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';

export default class VariableTimeFieldIndex extends Component {
@tracked days;
@tracked hours;
@tracked minutes;
@tracked data = this.createDataRow();

constructor() {
super(...arguments);
this.initializeTimeFields();
}

initializeTimeFields() {
let totalSeconds = this.args.time || 0;
this.days = Math.floor(totalSeconds / 86400);
totalSeconds %= 86400;
this.hours = Math.floor(totalSeconds / 3600);
totalSeconds %= 3600;
this.minutes = Math.floor(totalSeconds / 60);
this.data = this.createDataRow();
}

createDataRow() {
return [{ days: this.days, hours: this.hours, minutes: this.minutes }];
}

@action
updateTime(key, { target: { value } }) {
this.data[0][key] = Number(value);
const days = this.data[0].days;
const hours = this.data[0].hours;
const minutes = this.data[0].minutes;

let totalSeconds =
(days || 0) * 86400 + (hours || 0) * 3600 + (minutes || 0) * 60;
this.args.updateTime(totalSeconds);
}

@action
setMax() {
if (this.args.max == null) {
return;
}
let maxSeconds = this.args.max;
this.days = Math.floor(maxSeconds / 86400);
maxSeconds %= 86400;
this.hours = Math.floor(maxSeconds / 3600);
maxSeconds %= 3600;
this.minutes = Math.floor(maxSeconds / 60);
this.data = this.createDataRow();

this.args.updateTime(this.args.max);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/

import { module, test } from 'qunit';
import { setupRenderingTest } from 'admin/tests/helpers';
import { click, fillIn, render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setupIntl } from 'ember-intl/test-support';

module('Integration | Component | variable-time-field/index', function (hooks) {
setupRenderingTest(hooks);
setupIntl(hooks, 'en-us');

const DAYS_INPUT = 'input[name="days"]';
const HOURS_INPUT = 'input[name="hours"]';
const MINUTES_INPUT = 'input[name="minutes"]';
const SET_TO_MAX_BUTTON = 'button';
const LEGEND = 'Test Legend';
const ONE_HOUR = 3600; // in seconds
const ONE_DAY = 86400; // in seconds

hooks.beforeEach(function () {
this.set('legend', LEGEND);
this.set('time', ONE_HOUR);
this.set('max', ONE_DAY);
this.set('updateTime', () => {});
});

test('it renders', async function (assert) {
await render(hbs`<VariableTimeField
@legend={{this.legend}}
@isRequired={{true}}
@time={{this.time}}
@updateTime={{this.updateTime}}
/>`);

assert.dom('legend').containsText(LEGEND);
assert.dom(DAYS_INPUT).hasValue('0');
assert.dom(HOURS_INPUT).hasValue('1');
assert.dom(MINUTES_INPUT).hasValue('0');

await render(hbs`
<VariableTimeField
@legend={{this.legend}}
@max={{this.max}}
@time={{this.time}}
@updateTime={{this.updateTime}}
/>
`);

assert.dom(SET_TO_MAX_BUTTON).hasText('Set to Max');
assert.dom(DAYS_INPUT).hasValue('0');
assert.dom(HOURS_INPUT).hasValue('1');
assert.dom(MINUTES_INPUT).hasValue('0');
});

const testCases = [
{
input: DAYS_INPUT,
value: '2',
expectedSeconds: 176400, // 2 days + 1 hour
expectedValues: { days: '2', hours: '1', minutes: '0' },
},
{
input: HOURS_INPUT,
value: '2',
expectedSeconds: 7200, // 2 hours
expectedValues: { days: '0', hours: '2', minutes: '0' },
},
{
input: MINUTES_INPUT,
value: '15',
expectedSeconds: 4500, // 2 days + 1 minute
expectedValues: { days: '0', hours: '1', minutes: '15' },
},
];

test.each(
'it updates time on change',
testCases,
async function (assert, { input, value, expectedSeconds, expectedValues }) {
this.set('updateTime', (seconds) => {
if (seconds === expectedSeconds) {
assert.ok(
true,
`updateTime called with correct calculated seconds value: ${seconds}`,
);
} else {
assert.ok(
false,
`Unexpected seconds value: ${seconds}, expected: ${expectedSeconds}`,
);
}
});

await render(hbs`<VariableTimeField
@legend={{this.legend}}
@time={{this.time}}
@updateTime={{this.updateTime}}
/>`);

assert.dom(DAYS_INPUT).hasValue('0');
assert.dom(HOURS_INPUT).hasValue('1');
assert.dom(MINUTES_INPUT).hasValue('0');

await fillIn(input, value);

assert.dom(DAYS_INPUT).hasValue(expectedValues.days);
assert.dom(HOURS_INPUT).hasValue(expectedValues.hours);
assert.dom(MINUTES_INPUT).hasValue(expectedValues.minutes);
},
);

test('it sets to max time on button click', async function (assert) {
assert.expect(4);
this.set('legend', 'Test Legend');
this.set('time', 0);
this.set('max', 90061); // 1 day, 1 hour, 1 minute, and 1 second in seconds
this.set('updateTime', (totalSeconds) => {
assert.strictEqual(
totalSeconds,
90061,
'updateTime called with max total seconds',
);
});

await render(hbs`<VariableTimeField
@legend={{this.legend}}
@time={{this.time}}
@max={{this.max}}
@updateTime={{this.updateTime}}
/>`);

await click('button');

assert.dom('input[name="days"]').hasValue('1');
assert.dom('input[name="hours"]').hasValue('1');
assert.dom('input[name="minutes"]').hasValue('1');
});
});
Loading