Skip to content
Open
Show file tree
Hide file tree
Changes from all 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>
68 changes: 68 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,68 @@
/**
* 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';

const SECONDS_PER_MINUTE = 60;
const SECONDS_PER_HOUR = 3600;
const SECONDS_PER_DAY = 86400;

export default class VariableTimeFieldIndex extends Component {
@tracked days;
@tracked hours;
@tracked minutes;
@tracked data;

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

initializeTimeFields() {
let totalSeconds = this.args.time || 0;
this.days = Math.floor(totalSeconds / SECONDS_PER_DAY);
totalSeconds %= SECONDS_PER_DAY;
this.hours = Math.floor(totalSeconds / SECONDS_PER_HOUR);
totalSeconds %= SECONDS_PER_HOUR;
this.minutes = Math.floor(totalSeconds / SECONDS_PER_MINUTE);
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) * SECONDS_PER_DAY +
(hours || 0) * SECONDS_PER_HOUR +
(minutes || 0) * SECONDS_PER_MINUTE;
this.args.updateTime(totalSeconds);
}

@action
setMax() {
if (this.args.max == null) {
return;
}
let maxSeconds = this.args.max;
this.days = Math.floor(maxSeconds / SECONDS_PER_DAY);
maxSeconds %= SECONDS_PER_DAY;
this.hours = Math.floor(maxSeconds / SECONDS_PER_HOUR);
maxSeconds %= SECONDS_PER_HOUR;
this.minutes = Math.floor(maxSeconds / SECONDS_PER_MINUTE);
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