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>
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();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does data need to be initialized if we already did so in initializeTimeFields in the constructor


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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts on converting all the values used to calculate the different units of time (e.g. 86400, 3600, 60) into const vars? Since they are used more than once and it could help w/ readability.

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