@fleetbase/ember-ui 0.3.11 → 0.3.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/addon/components/chart.js +10 -10
- package/addon/components/click-to-reveal.hbs +1 -1
- package/addon/components/click-to-reveal.js +5 -23
- package/addon/components/dashboard/create.hbs +5 -3
- package/addon/components/dashboard/widget-panel.hbs +17 -7
- package/addon/components/dashboard/widget-panel.js +61 -13
- package/addon/components/dashboard.js +0 -1
- package/addon/components/layout/sidebar/item.hbs +2 -2
- package/addon/components/lazy-engine-component.hbs +8 -0
- package/addon/components/lazy-engine-component.js +136 -0
- package/addon/components/load-engine.hbs +1 -0
- package/addon/components/load-engine.js +82 -0
- package/addon/components/modals/query-builder-computed-column-editor.js +102 -7
- package/addon/components/pill.js +0 -1
- package/addon/components/query-builder/computed-columns.js +0 -2
- package/addon/components/registry-yield.hbs +7 -1
- package/addon/components/registry-yield.js +107 -15
- package/addon/components/report-builder.js +2 -1
- package/addon/components/tab-navigation.hbs +78 -38
- package/addon/components/tab-navigation.js +22 -9
- package/addon/components/table.hbs +20 -13
- package/addon/helpers/avatar-url.js +9 -0
- package/addon/helpers/lazy-engine-component.js +116 -0
- package/addon/helpers/point-coordinates.js +10 -0
- package/addon/helpers/point-to-coordinates.js +32 -0
- package/addon/helpers/resolve-component.js +18 -0
- package/addon/helpers/set-object-kv.js +9 -0
- package/addon/helpers/unwrap-coordinates.js +249 -0
- package/addon/services/dashboard.js +39 -17
- package/addon/styles/components/table.css +57 -0
- package/app/components/lazy-engine-component.js +1 -0
- package/app/components/load-engine.js +1 -0
- package/app/helpers/avatar-url.js +1 -0
- package/app/helpers/lazy-engine-component.js +1 -0
- package/app/helpers/point-coordinates.js +1 -0
- package/app/helpers/point-to-coordinates.js +1 -0
- package/app/helpers/set-object-kv.js +1 -0
- package/app/helpers/unwrap-coordinates.js +1 -0
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Component from '@glimmer/component';
|
|
2
2
|
import { tracked } from '@glimmer/tracking';
|
|
3
3
|
import { action } from '@ember/object';
|
|
4
|
+
import { debug } from '@ember/debug';
|
|
4
5
|
import Chart, { _adapters } from 'chart.js/auto';
|
|
5
6
|
import {
|
|
6
7
|
parse,
|
|
@@ -46,13 +47,7 @@ import {
|
|
|
46
47
|
|
|
47
48
|
export default class ChartComponent extends Component {
|
|
48
49
|
@tracked chart;
|
|
49
|
-
@tracked isLoading;
|
|
50
|
-
|
|
51
|
-
constructor() {
|
|
52
|
-
super(...arguments);
|
|
53
|
-
|
|
54
|
-
this.isLoading = this.args.isLoading === true;
|
|
55
|
-
}
|
|
50
|
+
@tracked isLoading = this.args.isLoading;
|
|
56
51
|
|
|
57
52
|
@action async renderChart(ctx) {
|
|
58
53
|
const options = {
|
|
@@ -65,9 +60,14 @@ export default class ChartComponent extends Component {
|
|
|
65
60
|
};
|
|
66
61
|
|
|
67
62
|
if (typeof options.data.datasets === 'function') {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
try {
|
|
64
|
+
this.isLoading = true;
|
|
65
|
+
options.data.datasets = await options.data.datasets();
|
|
66
|
+
} catch (err) {
|
|
67
|
+
debug('Error loading Chart dataset: ' + err.message);
|
|
68
|
+
} finally {
|
|
69
|
+
this.isLoading = false;
|
|
70
|
+
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
this.useDateFns();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<div class="relative click-to-reveal {{if this.isVisible 'is-visible' 'is-blurred'}}" {{on "click" (fn this.copy @value)}} ...attributes>
|
|
1
|
+
<div class="relative click-to-reveal {{this.wrapperClass}} {{if this.isVisible 'is-visible' 'is-blurred'}}" {{on "click" (fn this.copy @value)}} ...attributes>
|
|
2
2
|
<span class="click-to-reveal--hidden-value">
|
|
3
3
|
{{#if (has-block)}}
|
|
4
4
|
{{yield}}
|
|
@@ -4,33 +4,11 @@ import { action, computed } from '@ember/object';
|
|
|
4
4
|
import { later } from '@ember/runloop';
|
|
5
5
|
|
|
6
6
|
export default class ClickToRevealComponent extends ClickToCopyComponent {
|
|
7
|
-
/**
|
|
8
|
-
* The visiblity state of the value
|
|
9
|
-
*
|
|
10
|
-
* @var {Boolean}
|
|
11
|
-
*/
|
|
12
7
|
@tracked isVisible = false;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* The loading state of the reveal process
|
|
16
|
-
*
|
|
17
|
-
* @var {Boolean}
|
|
18
|
-
*/
|
|
19
8
|
@tracked isLoading = false;
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* The loading timing
|
|
23
|
-
*
|
|
24
|
-
* @var {Integer}
|
|
25
|
-
*/
|
|
26
9
|
@tracked timeout = 600;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* If click to copy should be enabled.
|
|
30
|
-
*
|
|
31
|
-
* @var {Boolean}
|
|
32
|
-
*/
|
|
33
10
|
@tracked clickToCopy = false;
|
|
11
|
+
@tracked wrapperClass = this.args.wrapperClass ?? '';
|
|
34
12
|
|
|
35
13
|
/**
|
|
36
14
|
* Setup the component
|
|
@@ -47,6 +25,10 @@ export default class ClickToRevealComponent extends ClickToCopyComponent {
|
|
|
47
25
|
if (column && column.cellComponentArgs) {
|
|
48
26
|
this.clickToCopy = column.cellComponentArgs.clickToCopy ?? false;
|
|
49
27
|
}
|
|
28
|
+
|
|
29
|
+
if (column && column.cellComponentArgs) {
|
|
30
|
+
this.wrapperClass = column.cellComponentArgs.wrapperClass ?? '';
|
|
31
|
+
}
|
|
50
32
|
}
|
|
51
33
|
|
|
52
34
|
/**
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
<div class="fleetbase-dashboard-grid" ...attributes>
|
|
2
2
|
<GridStack @options={{this.gridOptions}} @onChange={{this.onChangeGrid}}>
|
|
3
3
|
{{#each @dashboard.widgets as |widget|}}
|
|
4
|
-
{{#let (resolve-component widget.component) as |
|
|
5
|
-
{{#if
|
|
4
|
+
{{#let (resolve-component widget.component) as |componentDefinition|}}
|
|
5
|
+
{{#if componentDefinition}}
|
|
6
6
|
<GridStackItem id={{widget.id}} @options={{spread-widget-options (hash id=widget.id options=widget.grid_options)}} class="relative">
|
|
7
|
-
{{
|
|
7
|
+
<LazyEngineComponent @component={{componentDefinition}} as |resolvedComponent|>
|
|
8
|
+
{{component resolvedComponent widget=widget options=widget.options}}
|
|
9
|
+
</LazyEngineComponent>
|
|
8
10
|
|
|
9
11
|
{{#if @isEdit}}
|
|
10
12
|
<div class="absolute top-2 right-2">
|
|
@@ -1,25 +1,35 @@
|
|
|
1
1
|
<Overlay @isOpen={{@isOpen}} @onLoad={{this.setOverlayContext}} @position="right" @noBackdrop={{true}} @fullHeight={{true}} @width={{or this.width @width "400px"}}>
|
|
2
|
-
<Overlay::Header @title={{t "
|
|
2
|
+
<Overlay::Header @title={{t "dashboard-widget-panel.select-widgets"}} @hideStatusDot={{true}} @titleWrapperClass="leading-5 text-gray-800 dark:text-white">
|
|
3
3
|
<:actions>
|
|
4
4
|
<div class="flex flex-1 justify-end">
|
|
5
|
-
<Button @type="default" @icon="times" @helpText={{t "
|
|
5
|
+
<Button @type="default" @icon="times" @helpText={{t "dashboard-widget-panel.close-and-save"}} @onClick={{this.onPressClose}} />
|
|
6
6
|
</div>
|
|
7
7
|
</:actions>
|
|
8
8
|
</Overlay::Header>
|
|
9
9
|
|
|
10
|
-
<Overlay::Body @wrapperClass="new-service-rate-overlay-body px-
|
|
10
|
+
<Overlay::Body @wrapperClass="new-service-rate-overlay-body px-1 space-y-4 pt-2">
|
|
11
|
+
<div>
|
|
12
|
+
<Input
|
|
13
|
+
@type="text"
|
|
14
|
+
@value={{this.searchQuery}}
|
|
15
|
+
placeholder={{or (t "dashboard-widget-panel.filter-widgets") "Filter widgets by keyword"}}
|
|
16
|
+
class="w-full form-input form-input-sm"
|
|
17
|
+
{{on "input" this.updateSearchQuery}}
|
|
18
|
+
/>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
11
21
|
<div class="grid grid-cols-1 gap-4 text-xs dark:text-gray-100">
|
|
12
22
|
{{#each this.availableWidgets as |widget|}}
|
|
13
23
|
<div
|
|
14
24
|
class="rounded-lg border border-gray-300 bg-white dark:border-gray-700 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-300 ease-in-out shadow-md px-4 py-2 cursor-pointer"
|
|
15
|
-
{{on "click" (
|
|
25
|
+
{{on "click" (perform this.addWidgetToDashboard widget)}}
|
|
16
26
|
>
|
|
17
|
-
<div class="flex flex-row items-center leading-
|
|
18
|
-
<div class="w-
|
|
27
|
+
<div class="flex flex-row items-center leading-7 mb-2">
|
|
28
|
+
<div class="w-7 flex items-center justify-start">
|
|
19
29
|
<FaIcon @icon={{widget.icon}} class="text-lg text-gray-600 dark:text-gray-300" />
|
|
20
30
|
</div>
|
|
21
31
|
<p class="text-sm truncate font-semibold dark:text-gray-100 text-gray-800">
|
|
22
|
-
{{t "
|
|
32
|
+
{{t "dashboard-widget-panel.widget-name" widgetName=widget.name}}
|
|
23
33
|
</p>
|
|
24
34
|
</div>
|
|
25
35
|
<div>
|
|
@@ -2,23 +2,72 @@ import Component from '@glimmer/component';
|
|
|
2
2
|
import { tracked } from '@glimmer/tracking';
|
|
3
3
|
import { inject as service } from '@ember/service';
|
|
4
4
|
import { action } from '@ember/object';
|
|
5
|
+
import { task } from 'ember-concurrency';
|
|
6
|
+
import { ExtensionComponent } from '@fleetbase/ember-core/contracts';
|
|
5
7
|
|
|
6
8
|
export default class DashboardWidgetPanelComponent extends Component {
|
|
7
|
-
@service
|
|
8
|
-
@
|
|
9
|
+
@service('universe/widget-service') widgetService;
|
|
10
|
+
@service notifications;
|
|
11
|
+
@tracked defaultDashboardId = this.args.defaultDashboardId ?? 'dashboard';
|
|
9
12
|
@tracked dashboard;
|
|
10
13
|
@tracked isOpen = true;
|
|
11
|
-
@
|
|
14
|
+
@tracked searchQuery = '';
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
17
|
* Constructs the component and applies initial state.
|
|
15
18
|
*/
|
|
16
19
|
constructor(owner, { dashboard, defaultDashboardId = 'dashboard' }) {
|
|
17
20
|
super(...arguments);
|
|
18
|
-
this.
|
|
21
|
+
this.defaultDashboardId = defaultDashboardId;
|
|
19
22
|
this.dashboard = dashboard;
|
|
20
23
|
}
|
|
21
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Gets available widgets for the dashboard.
|
|
27
|
+
* Computed as a getter to ensure reactivity when widgets are registered.
|
|
28
|
+
* @returns {Array} Available widgets
|
|
29
|
+
*/
|
|
30
|
+
get availableWidgets() {
|
|
31
|
+
const dashboardId = this.args.defaultDashboardId || this.defaultDashboardId || 'dashboard';
|
|
32
|
+
const widgets = this.widgetService.getWidgets(dashboardId);
|
|
33
|
+
|
|
34
|
+
// Filter widgets by search query
|
|
35
|
+
if (this.searchQuery && this.searchQuery.trim()) {
|
|
36
|
+
const query = this.searchQuery.toLowerCase().trim();
|
|
37
|
+
return widgets.filter((widget) => {
|
|
38
|
+
const name = (widget.name || '').toLowerCase();
|
|
39
|
+
const description = (widget.description || '').toLowerCase();
|
|
40
|
+
return name.includes(query) || description.includes(query);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return widgets;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Adds a new available widget to the current dashboard.
|
|
49
|
+
*
|
|
50
|
+
* @param {Component|Widget|string} widget
|
|
51
|
+
* @memberof DashboardWidgetPanelComponent
|
|
52
|
+
*/
|
|
53
|
+
@task *addWidgetToDashboard(widget) {
|
|
54
|
+
// If widget is a component definition/class
|
|
55
|
+
if (typeof widget.component === 'function') {
|
|
56
|
+
widget.component = widget.component.name;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// If using extension component definition
|
|
60
|
+
if (widget.component instanceof ExtensionComponent) {
|
|
61
|
+
widget.component = widget.component.toString();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
yield this.dashboard.addWidget(widget);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
this.notifications.serverError(err);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
22
71
|
/**
|
|
23
72
|
* Sets the overlay context.
|
|
24
73
|
*
|
|
@@ -33,15 +82,14 @@ export default class DashboardWidgetPanelComponent extends Component {
|
|
|
33
82
|
}
|
|
34
83
|
}
|
|
35
84
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
});
|
|
85
|
+
/**
|
|
86
|
+
* Updates the search query
|
|
87
|
+
*
|
|
88
|
+
* @action
|
|
89
|
+
* @param {Event} event
|
|
90
|
+
*/
|
|
91
|
+
@action updateSearchQuery(event) {
|
|
92
|
+
this.searchQuery = event.target.value;
|
|
45
93
|
}
|
|
46
94
|
|
|
47
95
|
/**
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
>
|
|
10
10
|
<div class="next-nav-item-icon-container {{@iconWrapperClass}}">
|
|
11
11
|
{{#if @iconComponent}}
|
|
12
|
-
{{component @iconComponent options=@iconComponentOptions}}
|
|
12
|
+
{{component (lazy-engine-component @iconComponent) options=@iconComponentOptions}}
|
|
13
13
|
{{else if @icon}}
|
|
14
14
|
<FaIcon @prefix={{@iconPrefix}} @icon={{@icon}} @size={{or @iconSize "xs"}} class={{@iconClass}} />
|
|
15
15
|
{{/if}}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
<div class="truncate w-10/12 {{@itemWrapperClass}}">{{yield}}</div>
|
|
18
18
|
<div class="next-nav-item-right-side {{@itemRightSideContainerClass}}">
|
|
19
19
|
{{#if @rightSideComponent}}
|
|
20
|
-
{{component @rightSideComponent context=@rightSideComponentContext}}
|
|
20
|
+
{{component (lazy-engine-component @rightSideComponent) context=@rightSideComponentContext}}
|
|
21
21
|
{{else}}
|
|
22
22
|
{{#if @rightSideStatus}}
|
|
23
23
|
<span class="ml-2 {{@rightSideStatusContainerClass}}">
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import Component from '@glimmer/component';
|
|
2
|
+
import { inject as service } from '@ember/service';
|
|
3
|
+
import { tracked } from '@glimmer/tracking';
|
|
4
|
+
import { action } from '@ember/object';
|
|
5
|
+
import { assert } from '@ember/debug';
|
|
6
|
+
import { task } from 'ember-concurrency';
|
|
7
|
+
import { ExtensionComponent } from '@fleetbase/ember-core/contracts';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* LazyEngineComponent
|
|
11
|
+
*
|
|
12
|
+
* A wrapper component that handles lazy loading of components from engines.
|
|
13
|
+
* This component takes an ExtensionComponent definition and:
|
|
14
|
+
* 1. Triggers lazy loading of the engine if not already loaded
|
|
15
|
+
* 2. Looks up the component from the loaded engine
|
|
16
|
+
* 3. Renders the component with all passed arguments
|
|
17
|
+
*
|
|
18
|
+
* This enables cross-engine component usage while preserving lazy loading.
|
|
19
|
+
*
|
|
20
|
+
* @class LazyEngineComponent
|
|
21
|
+
* @extends Component
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* <LazyEngineComponent
|
|
25
|
+
* @componentDef={{this.menuItem.component}}
|
|
26
|
+
* @model={{@model}}
|
|
27
|
+
* @onChange={{@onChange}}
|
|
28
|
+
* />
|
|
29
|
+
*/
|
|
30
|
+
export default class LazyEngineComponent extends Component {
|
|
31
|
+
@service('universe/extension-manager') extensionManager;
|
|
32
|
+
@tracked resolvedComponent = null;
|
|
33
|
+
@tracked component = this.args.component;
|
|
34
|
+
@tracked params = this.args.params ?? {};
|
|
35
|
+
@tracked error = null;
|
|
36
|
+
|
|
37
|
+
constructor() {
|
|
38
|
+
super(...arguments);
|
|
39
|
+
this.loadComponent.perform();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@action handleChange(el, [component, params = {}]) {
|
|
43
|
+
this.component = component;
|
|
44
|
+
this.params = params;
|
|
45
|
+
this.loadComponent.perform();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Load the component from the engine
|
|
50
|
+
*
|
|
51
|
+
* @method loadComponent
|
|
52
|
+
* @private
|
|
53
|
+
*/
|
|
54
|
+
@task *loadComponent() {
|
|
55
|
+
let componentDef = this.component;
|
|
56
|
+
|
|
57
|
+
// Handle string form component definition
|
|
58
|
+
if (typeof componentDef === 'string' && componentDef.startsWith('#extension-component')) {
|
|
59
|
+
/* eslint-disable no-unused-vars */
|
|
60
|
+
const [_, engineName, componentPathOrName] = componentDef.split(':');
|
|
61
|
+
componentDef = new ExtensionComponent(engineName, componentPathOrName);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Handle backward compatibility: if componentDef is already a class or string, use it directly
|
|
65
|
+
if (typeof componentDef === 'function' || typeof componentDef === 'string') {
|
|
66
|
+
this.resolvedComponent = componentDef;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Handle ExtensionComponent definitions
|
|
71
|
+
if (componentDef && componentDef.engine) {
|
|
72
|
+
try {
|
|
73
|
+
const { engine: engineName, path: componentPath, class: componentClass, isClass } = componentDef;
|
|
74
|
+
|
|
75
|
+
assert(`LazyEngineComponent requires an engine name in componentDef`, engineName);
|
|
76
|
+
|
|
77
|
+
// Ensure engine is loaded
|
|
78
|
+
const engineInstance = yield this.extensionManager.ensureEngineLoaded(engineName);
|
|
79
|
+
if (!engineInstance) {
|
|
80
|
+
throw new Error(`Failed to load engine '${engineName}'`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Handle component class (immediate)
|
|
84
|
+
if (isClass && componentClass) {
|
|
85
|
+
// Register component class to engine if not already registered
|
|
86
|
+
const dasherized = componentClass.name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
87
|
+
const componentKey = `component:${dasherized}`;
|
|
88
|
+
|
|
89
|
+
if (!engineInstance.hasRegistration(componentKey)) {
|
|
90
|
+
engineInstance.register(componentKey, componentClass);
|
|
91
|
+
// Also register with original name
|
|
92
|
+
engineInstance.register(`component:${componentClass.name}`, componentClass);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.resolvedComponent = componentClass;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Handle component path (lazy)
|
|
100
|
+
if (componentPath) {
|
|
101
|
+
// Clean the path and lookup the component
|
|
102
|
+
const cleanPath = componentPath.replace(/^components\//, '');
|
|
103
|
+
|
|
104
|
+
// First, check if the component is registered in the engine
|
|
105
|
+
const componentKey = `component:${cleanPath}`;
|
|
106
|
+
if (!engineInstance.hasRegistration(componentKey)) {
|
|
107
|
+
throw new Error(`Component '${cleanPath}' is not registered in engine '${engineName}'. ` + `Make sure the component exists and is properly exported.`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Lookup the component factory (not the instance)
|
|
111
|
+
const componentFactory = engineInstance.factoryFor(componentKey);
|
|
112
|
+
if (!componentFactory) {
|
|
113
|
+
throw new Error(`Component factory for '${cleanPath}' not found in engine '${engineName}'. ` + `The component may be registered but not properly defined.`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Get the component class from the factory
|
|
117
|
+
const resolvedClass = componentFactory.class;
|
|
118
|
+
if (!resolvedClass) {
|
|
119
|
+
throw new Error(`Component class for '${cleanPath}' is undefined in engine '${engineName}'.`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this.resolvedComponent = resolvedClass;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
throw new Error('ExtensionComponent requires either a path or class');
|
|
127
|
+
} catch (e) {
|
|
128
|
+
console.error('LazyEngineComponent: Error loading component:', e);
|
|
129
|
+
this.error = e.message;
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
// Invalid component definition
|
|
133
|
+
this.error = 'Invalid component definition. Expected an ExtensionComponent with engine and path/class properties.';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{{yield this.engineInstance this.isLoading this.error}}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import Component from '@glimmer/component';
|
|
2
|
+
import { inject as service } from '@ember/service';
|
|
3
|
+
import { tracked } from '@glimmer/tracking';
|
|
4
|
+
import { assert, debug } from '@ember/debug';
|
|
5
|
+
import { task } from 'ember-concurrency';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* LoadEngine Component
|
|
9
|
+
*
|
|
10
|
+
* A component that handles lazy loading of engines and yields the engine instance.
|
|
11
|
+
* This provides a flexible pattern for working with engine components.
|
|
12
|
+
*
|
|
13
|
+
* @class LoadEngineComponent
|
|
14
|
+
* @extends Component
|
|
15
|
+
*
|
|
16
|
+
* @example Basic usage
|
|
17
|
+
* <LoadEngine @engineName="@fleetbase/fleetops-engine" as |engine|>
|
|
18
|
+
* {{#if engine}}
|
|
19
|
+
* {{component "admin/navigator-app" engineInstance=engine}}
|
|
20
|
+
* {{/if}}
|
|
21
|
+
* </LoadEngine>
|
|
22
|
+
*
|
|
23
|
+
* @example With loading and error states
|
|
24
|
+
* <LoadEngine @engineName="@fleetbase/fleetops-engine" as |engine isLoading error|>
|
|
25
|
+
* {{#if isLoading}}
|
|
26
|
+
* <LoadingSpinner />
|
|
27
|
+
* {{else if error}}
|
|
28
|
+
* <ErrorMessage @message={{error}} />
|
|
29
|
+
* {{else if engine}}
|
|
30
|
+
* {{component "admin/navigator-app" engineInstance=engine}}
|
|
31
|
+
* {{/if}}
|
|
32
|
+
* </LoadEngine>
|
|
33
|
+
*/
|
|
34
|
+
export default class LoadEngineComponent extends Component {
|
|
35
|
+
@service('universe/extension-manager') extensionManager;
|
|
36
|
+
@tracked engineInstance = null;
|
|
37
|
+
@tracked error = null;
|
|
38
|
+
|
|
39
|
+
constructor() {
|
|
40
|
+
super(...arguments);
|
|
41
|
+
this.loadEngine.perform();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Load the engine
|
|
46
|
+
*
|
|
47
|
+
* @method loadEngine
|
|
48
|
+
* @private
|
|
49
|
+
*/
|
|
50
|
+
@task *loadEngine() {
|
|
51
|
+
const { engineName } = this.args;
|
|
52
|
+
|
|
53
|
+
assert(`LoadEngine requires an @engineName argument`, engineName);
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
debug(`[LoadEngine] Loading engine: ${engineName}`);
|
|
57
|
+
|
|
58
|
+
// Trigger lazy loading of the engine
|
|
59
|
+
const engine = yield this.extensionManager.ensureEngineLoaded(engineName);
|
|
60
|
+
|
|
61
|
+
if (!engine) {
|
|
62
|
+
throw new Error(`Failed to load engine '${engineName}'`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
debug(`[LoadEngine] Engine loaded successfully: ${engineName}`);
|
|
66
|
+
this.engineInstance = engine;
|
|
67
|
+
} catch (e) {
|
|
68
|
+
console.error('[LoadEngine] Error loading engine:', e);
|
|
69
|
+
this.error = e.message;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Whether the engine is currently loading
|
|
75
|
+
*
|
|
76
|
+
* @computed isLoading
|
|
77
|
+
* @returns {Boolean}
|
|
78
|
+
*/
|
|
79
|
+
get isLoading() {
|
|
80
|
+
return this.loadEngine.isRunning;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -46,22 +46,117 @@ export default class ModalsQueryBuilderComputedColumnEditorComponent extends Com
|
|
|
46
46
|
|
|
47
47
|
get allowedFunctions() {
|
|
48
48
|
return [
|
|
49
|
+
// Date/Time Functions
|
|
49
50
|
'DATEDIFF',
|
|
50
51
|
'DATE_ADD',
|
|
51
52
|
'DATE_SUB',
|
|
53
|
+
'NOW',
|
|
54
|
+
'CURDATE',
|
|
55
|
+
'CURTIME',
|
|
56
|
+
'YEAR',
|
|
57
|
+
'MONTH',
|
|
58
|
+
'DAY',
|
|
59
|
+
'HOUR',
|
|
60
|
+
'MINUTE',
|
|
61
|
+
'SECOND',
|
|
62
|
+
'DATE_FORMAT',
|
|
63
|
+
'LAST_DAY',
|
|
64
|
+
'DAYOFWEEK',
|
|
65
|
+
'DAYOFMONTH',
|
|
66
|
+
'DAYOFYEAR',
|
|
67
|
+
'WEEK',
|
|
68
|
+
'WEEKDAY',
|
|
69
|
+
'QUARTER',
|
|
70
|
+
'TIMESTAMPDIFF',
|
|
71
|
+
'TIMESTAMPADD',
|
|
72
|
+
'FROM_UNIXTIME',
|
|
73
|
+
'UNIX_TIMESTAMP',
|
|
74
|
+
'STR_TO_DATE',
|
|
75
|
+
'MAKEDATE',
|
|
76
|
+
'ADDDATE',
|
|
77
|
+
'SUBDATE',
|
|
78
|
+
|
|
79
|
+
// String Functions
|
|
52
80
|
'CONCAT',
|
|
53
|
-
'
|
|
54
|
-
'
|
|
55
|
-
'
|
|
56
|
-
'
|
|
57
|
-
'GREATEST',
|
|
58
|
-
'ROUND',
|
|
59
|
-
'ABS',
|
|
81
|
+
'CONCAT_WS',
|
|
82
|
+
'SUBSTRING',
|
|
83
|
+
'SUBSTR',
|
|
84
|
+
'REPLACE',
|
|
60
85
|
'UPPER',
|
|
61
86
|
'LOWER',
|
|
62
87
|
'TRIM',
|
|
88
|
+
'LTRIM',
|
|
89
|
+
'RTRIM',
|
|
63
90
|
'LENGTH',
|
|
91
|
+
'CHAR_LENGTH',
|
|
92
|
+
'LEFT',
|
|
93
|
+
'RIGHT',
|
|
94
|
+
'LPAD',
|
|
95
|
+
'RPAD',
|
|
96
|
+
'REVERSE',
|
|
97
|
+
'LOCATE',
|
|
98
|
+
'POSITION',
|
|
99
|
+
'INSTR',
|
|
100
|
+
'STRCMP',
|
|
101
|
+
|
|
102
|
+
// Numeric Functions
|
|
103
|
+
'ROUND',
|
|
104
|
+
'ABS',
|
|
105
|
+
'CEIL',
|
|
106
|
+
'CEILING',
|
|
107
|
+
'FLOOR',
|
|
108
|
+
'TRUNCATE',
|
|
109
|
+
'MOD',
|
|
110
|
+
'POW',
|
|
111
|
+
'POWER',
|
|
112
|
+
'SQRT',
|
|
113
|
+
'SIGN',
|
|
114
|
+
'RAND',
|
|
115
|
+
'PI',
|
|
116
|
+
'EXP',
|
|
117
|
+
'LN',
|
|
118
|
+
'LOG',
|
|
119
|
+
'LOG10',
|
|
120
|
+
'LOG2',
|
|
121
|
+
'DEGREES',
|
|
122
|
+
'RADIANS',
|
|
123
|
+
'SIN',
|
|
124
|
+
'COS',
|
|
125
|
+
'TAN',
|
|
126
|
+
'ASIN',
|
|
127
|
+
'ACOS',
|
|
128
|
+
'ATAN',
|
|
129
|
+
'ATAN2',
|
|
130
|
+
|
|
131
|
+
// Conditional/Logic Functions
|
|
132
|
+
'CASE',
|
|
133
|
+
'WHEN',
|
|
134
|
+
'THEN',
|
|
135
|
+
'ELSE',
|
|
136
|
+
'END',
|
|
137
|
+
'IF',
|
|
138
|
+
'IFNULL',
|
|
64
139
|
'NULLIF',
|
|
140
|
+
'COALESCE',
|
|
141
|
+
|
|
142
|
+
// Comparison Functions
|
|
143
|
+
'LEAST',
|
|
144
|
+
'GREATEST',
|
|
145
|
+
|
|
146
|
+
// Aggregate Functions
|
|
147
|
+
'COUNT',
|
|
148
|
+
'SUM',
|
|
149
|
+
'AVG',
|
|
150
|
+
'MIN',
|
|
151
|
+
'MAX',
|
|
152
|
+
'GROUP_CONCAT',
|
|
153
|
+
|
|
154
|
+
// Type Conversion
|
|
155
|
+
'CAST',
|
|
156
|
+
'CONVERT',
|
|
157
|
+
|
|
158
|
+
// Other Utility Functions
|
|
159
|
+
'INTERVAL',
|
|
65
160
|
];
|
|
66
161
|
}
|
|
67
162
|
|
package/addon/components/pill.js
CHANGED
|
@@ -19,7 +19,6 @@ export default class PillComponent extends Component {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
@action handleClick() {
|
|
22
|
-
console.log('handleClick called!', ...arguments);
|
|
23
22
|
if (typeof this.args.onClick === 'function') {
|
|
24
23
|
if (this.args.resource) {
|
|
25
24
|
this.args.onClick(this.args.resource, ...arguments);
|
|
@@ -53,10 +53,8 @@ export default class QueryBuilderComputedColumnsComponent extends Component {
|
|
|
53
53
|
modal.startLoading();
|
|
54
54
|
|
|
55
55
|
const editorComponent = modal.getOption('modalComponentInstance');
|
|
56
|
-
console.log('[editorComponent]', editorComponent);
|
|
57
56
|
if (editorComponent) {
|
|
58
57
|
const isValid = await editorComponent.validateExpression();
|
|
59
|
-
console.log('[isValid]', isValid);
|
|
60
58
|
if (isValid) {
|
|
61
59
|
const computedColumn = editorComponent.save();
|
|
62
60
|
this.saveComputedColumn(computedColumn);
|
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
{{#each this.yieldables as |item|}}
|
|
2
|
-
{{
|
|
2
|
+
{{#if this.isComponent}}
|
|
3
|
+
<LazyEngineComponent @component={{item}} as |resolvedComponent|>
|
|
4
|
+
{{yield resolvedComponent @context}}
|
|
5
|
+
</LazyEngineComponent>
|
|
6
|
+
{{else}}
|
|
7
|
+
{{yield item @context}}
|
|
8
|
+
{{/if}}
|
|
3
9
|
{{/each}}
|