Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
f81d972
Let the dummy app resolve the ember-core imports the addon already makes
roncodes Sep 15, 2026
815bb75
Stub the universe registry and widget services in the dummy app
roncodes Sep 15, 2026
03f2930
Apply @class to the popover's floating element
roncodes Sep 15, 2026
5c4018d
Let an interactive popover opened by hover be entered
roncodes Sep 15, 2026
0868af9
Forward the popover options Attach::Tooltip dropped
roncodes Sep 15, 2026
2aee601
Render a pill with nothing to do as a static span, and hand the toolt…
roncodes Sep 15, 2026
2a38025
Forward the search and selected-item options MultiSelect dropped
roncodes Sep 15, 2026
5693211
Give the model filter an option component, and add a model-multiple f…
roncodes Sep 15, 2026
e4fc02e
Skip the identity cell's meta row when it would be empty
roncodes Sep 15, 2026
0bc3916
Add the resource descriptor registry
roncodes Sep 15, 2026
6ee1833
Tear the popover down with its listeners, and self-close a programmat…
roncodes Sep 15, 2026
24ed8a8
Add Resource::HoverCard and Resource::Summary
roncodes Sep 15, 2026
bc1e034
Add Resource::Pill
roncodes Sep 15, 2026
135f1ca
Add Table::Cell::Identity, the one-line identity cell
roncodes Sep 15, 2026
6132348
Add Resource::SelectOption
roncodes Sep 15, 2026
42e03af
Register descriptors for the console's core resources
roncodes Sep 15, 2026
7f5af2e
Add the user resource family
roncodes Sep 15, 2026
2b60468
Add the company resource family
roncodes Sep 15, 2026
1ddedd9
Add the group resource family
roncodes Sep 15, 2026
6837d18
Add the role resource family
roncodes Sep 15, 2026
9f6e7da
Add the file resource family
roncodes Sep 15, 2026
8998344
Add the category resource family
roncodes Sep 15, 2026
eaba9ac
Hide a fact pill's subtitle by its own class
roncodes Sep 15, 2026
2f52b26
Invoke has-block explicitly when passing it as an argument
roncodes Sep 15, 2026
d852e9a
Let a record's own type win over the wrapper's resource type
roncodes Sep 15, 2026
776c93f
Size the model-multiple filter trigger for chips
roncodes Sep 15, 2026
fec6e6a
Prepare the v0.4.1 release
roncodes Sep 15, 2026
b3ceb80
Satisfy lint for the resource identity work
roncodes Sep 15, 2026
227a524
Report a cleared model filter as a clear, and settle the dummy app fo…
roncodes Sep 15, 2026
90112a0
Keep the dummy app free of ember-core
roncodes Sep 15, 2026
6e41b60
Draw fallback UUID bytes from crypto, not Math.random
roncodes Sep 15, 2026
434196f
Fix CSS build performance with linked addons
roncodes Sep 16, 2026
d73baa7
Merge pull request #176 from fleetbase/feature/build-performance
roncodes Sep 16, 2026
b1ae649
Potential fix for pull request finding 'CodeQL / Incomplete string es…
roncodes Sep 16, 2026
0a35b87
Keep an interactive popover open while the pointer crosses into it
roncodes Sep 16, 2026
fc5c298
Compact the resource summary card and match the console theme
roncodes Sep 16, 2026
4d9d617
Give pills a fallback icon tile and hover padding
roncodes Sep 16, 2026
1b164a4
Show inline badges and status dots only where the resource has them
roncodes Sep 16, 2026
af2585f
Even out pill and identity images, and stop the column picker writing…
roncodes Sep 16, 2026
aebd786
Style every Fleet-Ops status badge and compact the select option
roncodes Sep 16, 2026
9c16695
Add a radio filter for two- or three-state choices
roncodes Sep 16, 2026
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
11 changes: 8 additions & 3 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
> v0.4.0 ~ "Tab navigation overflow fix"
> v0.4.1 ~ "Resource identity: pills, one-line identity cells, hover summaries and select options"

---
## Highlights

- Route-backed tabs without explicit IDs remain accessible through the TabNavigation More menu when the tab bar overflows (#171).
- Tab identity falls back from `id` to `route` to `key` for active state, overflow calculations, and keyboard navigation.
- **Resource descriptor registry** — `utils/resource-registry` stores one descriptor per kind of record (title, identifier, image, status, badges, facts, how it opens) in the shared `universe/registry-service`, so the host and every engine read one set. `resolveResourceKey` accepts a record, an identity stub, a model name or alias, a `prefix:key` string, a PHP class name or a polymorphic type; `openResource` unwraps proxies and stubs, swaps a subtype for its canonical record and never throws. The `resource-type`, `resource-component` and `resource-relation` helpers and the `resource-registry` service expose it to templates and engines.
- **Resource::Pill, Resource::Summary, Resource::HoverCard** — a descriptor-driven pill that opens its record and shows a compact summary card on hover (photo or icon, name and identifier, status and online state, a handful of facts and one View action). The card is lazy: nothing renders until the pointer has rested on the target, and it hydrates only then.
- **Table::Cell::Identity** — a one-line identity cell: a small image with the status dot, the name and up to two inline badges, no status badge. It opens the record on click, disables on `column.permission`, and never fires the row action as well.
- **Resource::SelectOption** — the option row for record pickers, with a compact variant for the closed trigger. `filter/model` renders options through it and the new `filter/model-multiple` filter selects several records.
- **Core resource families** — pills, summaries, select options and identity cells for user, company, group, role, file and category, registered by an instance-initializer in the host.
- **Fixes** — `Attach::Popover` applies `@class`, keeps an interactive popover enterable while shown, follows later changes to `@isShown` and removes its listeners on destroy; `Attach::Tooltip` forwards `hideDelay`, `hideDuration`, `@class`, `@style`, `floatingTarget`, `floatingContainer` and `onChange`; `Pill` renders a static span when it has nothing to do and hands its tooltip component the resource; `MultiSelect` forwards `@selectedItemComponent`, `@searchField` and `@searchPlaceholder`; `table/cell/resource-identity` no longer renders an empty meta row.
- **Dummy app** — `@fleetbase/ember-core` is declared as a dependency so the components that import it can render in tests.

---
## Need help?
Expand Down
5 changes: 3 additions & 2 deletions addon/components/attach/popover.hbs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
<div {{did-insert this.setupComponent}}>
<div {{did-insert this.setupComponent}} {{did-update this.isShownArgumentChanged @isShown}}>
{{#if (and this.currentTarget (or (not this.lazyRender) this.mustRender))}}
<Floating
id={{this.id}}
class="ember-attacher"
class="ember-attacher {{this.class}}"
@pointerEvents={{this.floatingPointerEvents}}
@ariaRole={{@ariaRole}}
@placement={{this.placement}}
@container={{this.floatingContainer}}
Expand Down
78 changes: 69 additions & 9 deletions addon/components/attach/popover.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { cancel, debounce, later, run } from '@ember/runloop';
export default class AttachPopoverComponent extends Component {
@tracked animation = 'fill';
@tracked arrow = false;
@tracked class = null;
@tracked flip = null;
@tracked hideDelay = 0;
@tracked hideDuration = 300;
Expand Down Expand Up @@ -47,10 +48,21 @@ export default class AttachPopoverComponent extends Component {
return `ember-attacher-${this.animation} ${showOrHideClass} ${arrowClass}`;
}

@computed('style', 'transitionDuration', 'isShown') get computedStyle() {
const { style, transitionDuration, isShown } = this;
@computed('style', 'transitionDuration', 'isShown', 'interactive') get computedStyle() {
const { style, transitionDuration, isShown, interactive } = this;

return htmlSafe(`transition-duration: ${transitionDuration}ms; pointer-events: ${isShown ? 'auto' : 'none'}; ${style ?? ''}`);
// Only an interactive attachment may be entered: a plain tooltip stays
// transparent to the pointer so hovering it never steals the target's
// mouseleave. `isShown` is maintained by show()/hide().
return htmlSafe(`transition-duration: ${transitionDuration}ms; pointer-events: ${isShown && interactive ? 'auto' : 'none'}; ${style ?? ''}`);
}

/**
* The pointer-events value the floating element itself should carry so an
* interactive attachment can be entered while it is visible.
*/
@computed('isShown', 'interactive') get floatingPointerEvents() {
return this.isShown && this.interactive ? 'auto' : 'none';
}

@computed('showOn') get showEvents() {
Expand Down Expand Up @@ -93,6 +105,10 @@ export default class AttachPopoverComponent extends Component {
// apply default arguments
this.setDefaultOptions();

// Whether the attachment should currently be visible; also driven by
// show()/hide() and, later, by updates to @isShown.
this.isShown = Boolean(this.args.isShown);

// set last used capture arg
this.lastUseCaptureArgumentValue = this.useCapture;

Expand Down Expand Up @@ -121,6 +137,18 @@ export default class AttachPopoverComponent extends Component {
this.initializeAttacher();
}

/**
* Follows later changes to `@isShown` so a parent can open or close the
* attachment after the initial render.
*/
@action isShownArgumentChanged(element, [isShown]) {
if (isShown) {
this.show();
} else if (this.floatingElement) {
this.hide();
}
}

@action initializeAttacher() {
this.removeEventListeners();

Expand All @@ -143,6 +171,14 @@ export default class AttachPopoverComponent extends Component {
@action hide() {
const { floatingElement } = this;

this.isShown = false;
this.hidePending = false;

if (this.hideListenersOnDocumentByEvent.mousemove) {
delete this.hideListenersOnDocumentByEvent.mousemove;
document.removeEventListener('mousemove', this.hideIfMouseOutsideTargetOrAttachment, this.useCapture);
}

if (!floatingElement) {
this.animationTimeout = requestAnimationFrame(() => {
this.animationTimeout = this.hide();
Expand Down Expand Up @@ -191,13 +227,19 @@ export default class AttachPopoverComponent extends Component {
return;
}

// If cursor is not on the attachment or target, hide the popover
if (!target.contains(event.target) && !(this.isOffset && this.isCursorBetweenTargetAndAttachment(event)) && this.floatingElement && !this.floatingElement.contains(event.target)) {
// Remove this listener before hiding the attachment
delete this.hideListenersOnDocumentByEvent.mousemove;
document.removeEventListener('mousemove', this.hideIfMouseOutsideTargetOrAttachment, this.useCapture);
const outside =
!target.contains(event.target) && !(this.isOffset && this.isCursorBetweenTargetAndAttachment(event)) && this.floatingElement && !this.floatingElement.contains(event.target);

this.hideAfterDelay();
if (outside) {
// Start the delayed hide once and keep listening, so re-entering the
// target or the attachment within the delay cancels it.
if (!this.hidePending) {
this.hidePending = true;
this.hideAfterDelay();
}
} else if (this.hidePending) {
cancel(this.delayedVisibilityToggle);
this.hidePending = false;
}
}

Expand Down Expand Up @@ -326,6 +368,7 @@ export default class AttachPopoverComponent extends Component {

@action showAfterDelay() {
cancel(this.delayedVisibilityToggle);
this.hidePending = false;

this.mustRender = true;
this.addListenersForHideEvents();
Expand All @@ -343,10 +386,25 @@ export default class AttachPopoverComponent extends Component {
}

this.mustRender = true;
this.isShown = true;

// Make the attachment visible immediately so transition animations can take place
this.setIsVisibleAfterDelay(true, 0);
this.startShowAnimation();

// An interactive attachment that was opened programmatically (or whose
// target the pointer left before the listeners were bound) must still
// close once the pointer is away from both target and attachment.
if (this.interactive && this.hideEvents.includes('mouseleave')) {
this.hideOnMouseLeaveTarget();
}
}

willDestroy() {
super.willDestroy(...arguments);
cancel(this.delayedVisibilityToggle);
cancelAnimationFrame(this.animationTimeout);
this.removeEventListeners();
}

@action startShowAnimation() {
Expand Down Expand Up @@ -416,6 +474,7 @@ export default class AttachPopoverComponent extends Component {
this.animationTimeout = requestAnimationFrame(() => {
if (!this.isDestroyed && !this.isDestroying) {
this.floatingElement.style.display = isVisible ? '' : 'none';
this.floatingElement.style.pointerEvents = this.floatingPointerEvents;

// Prevent jank by making the attachment invisible until positioned.
// The visibility style will be toggled by this.startShowAnimation()
Expand All @@ -431,6 +490,7 @@ export default class AttachPopoverComponent extends Component {
);
} else {
this.floatingElement.style.display = isVisible ? '' : 'none';
this.floatingElement.style.pointerEvents = this.floatingPointerEvents;

// Prevent jank by making the attachment invisible until positioned.
// The visibility style will be toggled by this.startShowAnimation()
Expand Down
7 changes: 7 additions & 0 deletions addon/components/attach/tooltip.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
@offset={{@offset}}
@shiftOptions={{@shiftOptions}}
@animation={{@animation}}
@class={{@class}}
@style={{@style}}
@floatingTarget={{@floatingTarget}}
@floatingContainer={{@floatingContainer}}
@hideDelay={{@hideDelay}}
@hideDuration={{@hideDuration}}
@hideOn={{@hideOn}}
@showOn={{@showOn}}
@interactive={{@interactive}}
Expand All @@ -15,5 +21,6 @@
@showDelay={{@showDelay}}
@showDuration={{@showDuration}}
@transitionDuration={{@transitionDuration}}
@onChange={{@onChange}}
...attributes
>{{yield}}</Attach::Popover>
24 changes: 24 additions & 0 deletions addon/components/category/pill.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Resource::Pill
@resource={{or @category @resource}}
@resourceType="category"
@title={{@title}}
@titleFallback={{or @titleFallback "No category"}}
@subtitle={{@subtitle}}
@showOnlineIndicator={{@showOnlineIndicator}}
@noPopover={{@noPopover}}
@popoverPlacement={{@popoverPlacement}}
@static={{@static}}
@onClick={{@onClick}}
@onView={{@onView}}
@anchorClass={{@anchorClass}}
@imageClass={{@imageClass}}
@imageWrapperClass={{@imageWrapperClass}}
@contentWrapperClass={{@contentWrapperClass}}
@titleClass={{@titleClass}}
@subtitleClass={{@subtitleClass}}
@useBlock={{(has-block)}}
...attributes
as |resource|
>
{{yield resource}}
</Resource::Pill>
1 change: 1 addition & 0 deletions addon/components/category/summary.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<Resource::Summary @resource={{or @category @resource}} @resourceType="category" @onView={{@onView}} @showView={{@showView}} @onClose={{@onClose}} @titleFallback={{@titleFallback}} ...attributes />
24 changes: 24 additions & 0 deletions addon/components/company/pill.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Resource::Pill
@resource={{or @company @resource}}
@resourceType="company"
@title={{@title}}
@titleFallback={{or @titleFallback "No company"}}
@subtitle={{@subtitle}}
@showOnlineIndicator={{@showOnlineIndicator}}
@noPopover={{@noPopover}}
@popoverPlacement={{@popoverPlacement}}
@static={{@static}}
@onClick={{@onClick}}
@onView={{@onView}}
@anchorClass={{@anchorClass}}
@imageClass={{@imageClass}}
@imageWrapperClass={{@imageWrapperClass}}
@contentWrapperClass={{@contentWrapperClass}}
@titleClass={{@titleClass}}
@subtitleClass={{@subtitleClass}}
@useBlock={{(has-block)}}
...attributes
as |resource|
>
{{yield resource}}
</Resource::Pill>
1 change: 1 addition & 0 deletions addon/components/company/summary.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<Resource::Summary @resource={{or @company @resource}} @resourceType="company" @onView={{@onView}} @showView={{@showView}} @onClose={{@onClose}} @titleFallback={{@titleFallback}} ...attributes />
24 changes: 24 additions & 0 deletions addon/components/file/pill.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Resource::Pill
@resource={{or @file @resource}}
@resourceType="file"
@title={{@title}}
@titleFallback={{or @titleFallback "No file"}}
@subtitle={{@subtitle}}
@showOnlineIndicator={{@showOnlineIndicator}}
@noPopover={{@noPopover}}
@popoverPlacement={{@popoverPlacement}}
@static={{@static}}
@onClick={{@onClick}}
@onView={{@onView}}
@anchorClass={{@anchorClass}}
@imageClass={{@imageClass}}
@imageWrapperClass={{@imageWrapperClass}}
@contentWrapperClass={{@contentWrapperClass}}
@titleClass={{@titleClass}}
@subtitleClass={{@subtitleClass}}
@useBlock={{(has-block)}}
...attributes
as |resource|
>
{{yield resource}}
</Resource::Pill>
1 change: 1 addition & 0 deletions addon/components/file/summary.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<Resource::Summary @resource={{or @file @resource}} @resourceType="file" @onView={{@onView}} @showView={{@showView}} @onClose={{@onClose}} @titleFallback={{@titleFallback}} ...attributes />
24 changes: 24 additions & 0 deletions addon/components/filter/model-multiple.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<div class="filter-model-multiple flex flex-row items-center" ...attributes>
<ModelSelectMultiple
@modelName={{@filter.model}}
@query={{@filter.query}}
@optionLabel={{this.optionLabel}}
@selectedModel={{this.selectedModels}}
@placeholder={{@placeholder}}
@triggerClass="form-select form-input form-input-sm flex-1"
@infiniteScroll={{false}}
@renderInPlace={{true}}
@onChange={{this.onChange}}
@selectedItemComponent={{if this.optionComponent (component this.optionComponent compact=true)}}
as |model|
>
{{#if this.optionComponent}}
{{component this.optionComponent option=model}}
{{else}}
{{get model this.optionLabel}}
{{/if}}
</ModelSelectMultiple>
<button type="button" class="clear-button" disabled={{not this.selectedModels.length}} aria-label="Clear" {{on "click" this.clear}}>
<FaIcon @icon="times" />
</button>
</div>
Loading
Loading