@fleetbase/ember-ui 0.3.6 → 0.3.8
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/custom-fields-manager.js +52 -9
- package/addon/components/dashboard.js +3 -3
- package/addon/components/filter/string.hbs +2 -2
- package/addon/components/filter/string.js +12 -0
- package/addon/components/layout/header/sidebar-toggle.js +5 -9
- package/addon/components/pill.hbs +48 -0
- package/addon/components/pill.js +31 -0
- package/addon/components/query-builder/column-select.hbs +7 -4
- package/addon/components/query-builder/column-select.js +20 -8
- package/addon/components/query-builder/conditions.js +2 -4
- package/addon/components/query-builder/group-by.hbs +2 -4
- package/addon/components/query-builder/group-by.js +65 -25
- package/addon/components/query-builder/joins.hbs +0 -7
- package/addon/components/query-builder/joins.js +0 -80
- package/addon/components/query-builder/limit.js +8 -7
- package/addon/components/query-builder/sort-by.hbs +1 -1
- package/addon/components/query-builder/sort-by.js +23 -15
- package/addon/components/report/details.hbs +3 -7
- package/addon/components/unit-input.js +1 -1
- package/addon/helpers/is-object-empty.js +6 -0
- package/addon/services/dashboard.js +14 -12
- package/addon/styles/components/badge.css +24 -0
- package/app/components/pill.js +1 -0
- package/app/helpers/is-object-empty.js +1 -0
- package/package.json +1 -1
|
@@ -33,7 +33,8 @@ export default class CustomFieldsManagerComponent extends Component {
|
|
|
33
33
|
super(...arguments);
|
|
34
34
|
this.subjects = subjects ?? [];
|
|
35
35
|
next(() => {
|
|
36
|
-
if (!this.subjects) return;
|
|
36
|
+
if (!this.subjects || this.subjects.length === 0) return;
|
|
37
|
+
// Load the first subject immediately
|
|
37
38
|
this.loadCustomFields.perform(this.subjects[0]);
|
|
38
39
|
});
|
|
39
40
|
}
|
|
@@ -51,7 +52,44 @@ export default class CustomFieldsManagerComponent extends Component {
|
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Restore custom fields from cache for subjects that haven't been loaded yet
|
|
57
|
+
* This method checks the CustomFieldsRegistry service cache and restores
|
|
58
|
+
* previously loaded data to avoid losing it during navigation
|
|
59
|
+
*/
|
|
60
|
+
async restoreFromCache() {
|
|
61
|
+
if (!this.subjects || this.subjects.length <= 1) return;
|
|
62
|
+
|
|
63
|
+
// Skip the first subject as it's loaded in constructor
|
|
64
|
+
const subjectsToRestore = this.subjects.slice(1);
|
|
65
|
+
|
|
66
|
+
for (const subject of subjectsToRestore) {
|
|
67
|
+
try {
|
|
68
|
+
const company = await this.currentUser.loadCompany();
|
|
69
|
+
const loadOptions = {
|
|
70
|
+
groupedFor: `${underscore(subject.model)}_custom_field_group`,
|
|
71
|
+
fieldFor: subject.type,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Check if we have a cached manager for this subject
|
|
75
|
+
const cachedManager = this.customFieldsRegistry.forSubject(company, { loadOptions });
|
|
76
|
+
|
|
77
|
+
// Only restore if we have cached groups data
|
|
78
|
+
if (cachedManager && cachedManager.groups && cachedManager.groups.length > 0) {
|
|
79
|
+
this.#updateSubject(subject, (s) => {
|
|
80
|
+
return { ...s, groups: cachedManager.groups };
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
} catch (err) {
|
|
84
|
+
// Silently continue if cache restore fails for a subject
|
|
85
|
+
console.warn(`Failed to restore cache for subject ${subject.model}:`, err);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
54
90
|
@task *loadCustomFields(subject) {
|
|
91
|
+
if (!subject) return;
|
|
92
|
+
|
|
55
93
|
try {
|
|
56
94
|
const company = yield this.currentUser.loadCompany();
|
|
57
95
|
const customFieldsManager = yield this.customFieldsRegistry.loadSubjectCustomFields.perform(company, {
|
|
@@ -60,12 +98,14 @@ export default class CustomFieldsManagerComponent extends Component {
|
|
|
60
98
|
fieldFor: subject.type,
|
|
61
99
|
},
|
|
62
100
|
});
|
|
101
|
+
|
|
63
102
|
this.#updateSubject(subject, (s) => {
|
|
64
103
|
return { ...s, groups: customFieldsManager.customFieldGroups };
|
|
65
104
|
});
|
|
66
105
|
|
|
67
106
|
return customFieldsManager;
|
|
68
107
|
} catch (err) {
|
|
108
|
+
console.error(`❌ Failed to load custom fields for ${subject.model}:`, err);
|
|
69
109
|
this.notifications.serverError(err);
|
|
70
110
|
}
|
|
71
111
|
}
|
|
@@ -127,7 +167,7 @@ export default class CustomFieldsManagerComponent extends Component {
|
|
|
127
167
|
|
|
128
168
|
try {
|
|
129
169
|
await group.destroyRecord();
|
|
130
|
-
await this.loadCustomFields.perform(subject);
|
|
170
|
+
await this.loadCustomFields.perform(subject, true); // Force reload after deletion
|
|
131
171
|
modal.done();
|
|
132
172
|
} catch (error) {
|
|
133
173
|
this.notifications.serverError(error);
|
|
@@ -151,7 +191,7 @@ export default class CustomFieldsManagerComponent extends Component {
|
|
|
151
191
|
|
|
152
192
|
try {
|
|
153
193
|
await customField.destroyRecord();
|
|
154
|
-
await this.loadCustomFields.perform(subject);
|
|
194
|
+
await this.loadCustomFields.perform(subject, true); // Force reload after deletion
|
|
155
195
|
modal.done();
|
|
156
196
|
} catch (error) {
|
|
157
197
|
this.notifications.serverError(error);
|
|
@@ -161,6 +201,13 @@ export default class CustomFieldsManagerComponent extends Component {
|
|
|
161
201
|
});
|
|
162
202
|
}
|
|
163
203
|
|
|
204
|
+
@action onTabChange(subject) {
|
|
205
|
+
// Ensure custom fields are loaded when switching tabs
|
|
206
|
+
if (subject && (!subject.groups || subject.groups.length === 0)) {
|
|
207
|
+
this.loadCustomFields.perform(subject);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
164
211
|
#addCustomFieldToGroup(subject, customField, group) {
|
|
165
212
|
this.#updateGroupOnSubject(subject, group.id, (g) => {
|
|
166
213
|
const current = isArray(g.customFields) ? g.customFields : [];
|
|
@@ -207,12 +254,8 @@ export default class CustomFieldsManagerComponent extends Component {
|
|
|
207
254
|
#updateSubject(subject, updater) {
|
|
208
255
|
if (!subject) return;
|
|
209
256
|
|
|
210
|
-
//
|
|
211
|
-
const
|
|
212
|
-
const idx = this.subjects.findIndex((s) => {
|
|
213
|
-
if (s === subject) return true;
|
|
214
|
-
return idKey ? s?.[idKey] === subject?.[idKey] : false;
|
|
215
|
-
});
|
|
257
|
+
// Find by model property since tab objects have extra properties
|
|
258
|
+
const idx = this.subjects.findIndex((s) => s?.model === subject?.model);
|
|
216
259
|
|
|
217
260
|
if (idx === -1) return;
|
|
218
261
|
|
|
@@ -23,12 +23,12 @@ export default class DashboardComponent extends Component {
|
|
|
23
23
|
* Creates an instance of DashboardComponent.
|
|
24
24
|
* @memberof DashboardComponent
|
|
25
25
|
*/
|
|
26
|
-
constructor(owner, { defaultDashboardId = 'dashboard', defaultDashboardName = 'Default Dashboard', showPanelWhenZeroWidgets = false }) {
|
|
26
|
+
constructor(owner, { defaultDashboardId = 'dashboard', defaultDashboardName = 'Default Dashboard', showPanelWhenZeroWidgets = false, extension = 'core' } = {}) {
|
|
27
27
|
super(...arguments);
|
|
28
28
|
this.dashboard.reset(); // ensure service is reset when re-rendering
|
|
29
29
|
next(() => {
|
|
30
30
|
this.dashboard.showPanelWhenZeroWidgets = showPanelWhenZeroWidgets;
|
|
31
|
-
this.dashboard.loadDashboards.perform(defaultDashboardId, defaultDashboardName);
|
|
31
|
+
this.dashboard.loadDashboards.perform({ defaultDashboardId, defaultDashboardName, extension });
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -63,7 +63,7 @@ export default class DashboardComponent extends Component {
|
|
|
63
63
|
// Get the name from the modal options
|
|
64
64
|
const { name } = modal.getOptions();
|
|
65
65
|
|
|
66
|
-
await this.dashboard.createDashboard.perform(name);
|
|
66
|
+
await this.dashboard.createDashboard.perform(name, { extension: this.args.extension });
|
|
67
67
|
done();
|
|
68
68
|
},
|
|
69
69
|
...options,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div class="filter-string" ...attributes>
|
|
2
|
-
<Input @value={{
|
|
3
|
-
<button type="button" class="clear-button" disabled={{not
|
|
2
|
+
<Input @value={{this.value}} placeholder={{@placeholder}} {{on "input" this.onChange}} aria-label="Filter Input" autocomplete="false" class="form-input form-input-sm flex-1" />
|
|
3
|
+
<button type="button" class="clear-button" disabled={{not this.value}} alt="Clear" {{on "click" this.clear}}>
|
|
4
4
|
<FaIcon @icon="times" />
|
|
5
5
|
</button>
|
|
6
6
|
</div>
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import Component from '@glimmer/component';
|
|
2
|
+
import { tracked } from '@glimmer/tracking';
|
|
2
3
|
import { action } from '@ember/object';
|
|
3
4
|
|
|
4
5
|
export default class FilterStringComponent extends Component {
|
|
6
|
+
@tracked value = '';
|
|
7
|
+
|
|
8
|
+
constructor(owner, { value = '' }) {
|
|
9
|
+
super(...arguments);
|
|
10
|
+
this.value = value;
|
|
11
|
+
}
|
|
12
|
+
|
|
5
13
|
@action onChange({ target: { value } }) {
|
|
6
14
|
const { onChange, filter } = this.args;
|
|
7
15
|
|
|
16
|
+
this.value = value;
|
|
17
|
+
|
|
8
18
|
if (typeof onChange === 'function') {
|
|
9
19
|
onChange(filter, value);
|
|
10
20
|
}
|
|
@@ -13,6 +23,8 @@ export default class FilterStringComponent extends Component {
|
|
|
13
23
|
@action clear() {
|
|
14
24
|
const { onClear, filter } = this.args;
|
|
15
25
|
|
|
26
|
+
this.value = '';
|
|
27
|
+
|
|
16
28
|
if (typeof onClear === 'function') {
|
|
17
29
|
onClear(filter);
|
|
18
30
|
}
|
|
@@ -5,25 +5,21 @@ import { tracked } from '@glimmer/tracking';
|
|
|
5
5
|
|
|
6
6
|
export default class LayoutHeaderSidebarToggleComponent extends Component {
|
|
7
7
|
@service universe;
|
|
8
|
+
@service sidebar;
|
|
8
9
|
@tracked isSidebarVisible = true;
|
|
9
10
|
|
|
10
11
|
@action toggleSidebar() {
|
|
11
|
-
if (this.args.disabled === true)
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const sidebar = this.universe.sidebarContext;
|
|
12
|
+
if (this.args.disabled === true) return;
|
|
16
13
|
|
|
17
14
|
if (this.isSidebarVisible) {
|
|
18
|
-
sidebar.hideNow();
|
|
15
|
+
this.sidebar.hideNow();
|
|
19
16
|
} else {
|
|
20
|
-
sidebar.show();
|
|
17
|
+
this.sidebar.show();
|
|
21
18
|
}
|
|
22
19
|
|
|
23
20
|
this.isSidebarVisible = !this.isSidebarVisible;
|
|
24
|
-
|
|
25
21
|
if (typeof this.args.onToggle === 'function') {
|
|
26
|
-
this.args.onToggle(sidebar, this.isSidebarVisible);
|
|
22
|
+
this.args.onToggle(this.sidebar, this.isSidebarVisible);
|
|
27
23
|
}
|
|
28
24
|
}
|
|
29
25
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<div class="fleetbase-pill" ...attributes>
|
|
2
|
+
<a href="javascript:;" class="flex flex-row space-x-2 {{@anchorClass}}" {{on "click" this.handleClick}}>
|
|
3
|
+
<div class="relative shrink-0 {{@imageWrapperClass}}">
|
|
4
|
+
{{#if (has-block "image")}}
|
|
5
|
+
{{yield @resource to="image"}}
|
|
6
|
+
{{else}}
|
|
7
|
+
<Image
|
|
8
|
+
src={{@imageSrc}}
|
|
9
|
+
@fallbackSrc={{or @imageFallback (config (concat "defaultValues." @fallbackImageType)) (config "defaultValues.placeholderImage")}}
|
|
10
|
+
width={{or @imageSize @imageWidth "28"}}
|
|
11
|
+
height={{or @imageSize @imageHeight "28"}}
|
|
12
|
+
class="w-7 h-7 rounded-full ring-2 ring-gray-800 dark:ring-gray-700 shadow transition-shadow hover:shadow-md focus:shadow-md {{@imageClass}}"
|
|
13
|
+
alt={{or @imageAlt this.resourceName}}
|
|
14
|
+
/>
|
|
15
|
+
{{#if @showOnlineIndicator}}
|
|
16
|
+
<FaIcon
|
|
17
|
+
@icon="circle"
|
|
18
|
+
@size="2xs"
|
|
19
|
+
class="absolute left-0 top-0 h-2 w-2 {{if (get @resource (or @onlinePath 'online')) 'text-green-500' 'text-yellow-200'}} {{@onlineIndicatorClass}}"
|
|
20
|
+
/>
|
|
21
|
+
{{/if}}
|
|
22
|
+
{{yield @resource to="image"}}
|
|
23
|
+
{{/if}}
|
|
24
|
+
</div>
|
|
25
|
+
<div class={{@contentWrapperClass}}>
|
|
26
|
+
{{#if (has-block)}}
|
|
27
|
+
{{yield @resource}}
|
|
28
|
+
{{else}}
|
|
29
|
+
<div class="text-sm {{@titleClass}}">{{n-a @title this.resourceName}}</div>
|
|
30
|
+
{{#if @subtitle}}
|
|
31
|
+
<div class="text-xs text-gray-400 dark:text-gray-500 {{@subtitleClass}}">{{n-a @subtitle}}</div>
|
|
32
|
+
{{/if}}
|
|
33
|
+
{{yield @resource}}
|
|
34
|
+
{{/if}}
|
|
35
|
+
</div>
|
|
36
|
+
{{#if (has-block "tooltip")}}
|
|
37
|
+
<Attach::Tooltip @class="clean" @animation="scale" @placement={{or @tooltipPosition "top"}}>
|
|
38
|
+
<InputInfo>
|
|
39
|
+
{{yield @resource to="tooltip"}}
|
|
40
|
+
</InputInfo>
|
|
41
|
+
</Attach::Tooltip>
|
|
42
|
+
{{else if @tooltipComponent}}
|
|
43
|
+
<Attach::Tooltip @class="clean" @animation="scale" @placement={{or @tooltipPosition "top"}}>
|
|
44
|
+
{{component @tooltipComponent}}
|
|
45
|
+
</Attach::Tooltip>
|
|
46
|
+
{{/if}}
|
|
47
|
+
</a>
|
|
48
|
+
</div>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import Component from '@glimmer/component';
|
|
2
|
+
import { action, get } from '@ember/object';
|
|
3
|
+
import getModelName from '@fleetbase/ember-core/utils/get-model-name';
|
|
4
|
+
|
|
5
|
+
export default class PillComponent extends Component {
|
|
6
|
+
/* eslint-disable ember/no-get */
|
|
7
|
+
get resourceName() {
|
|
8
|
+
const record = this.args.resource;
|
|
9
|
+
if (!record) return 'resource';
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
get(record, this.args.namePath ?? 'name') ??
|
|
13
|
+
get(record, 'display_name') ??
|
|
14
|
+
get(record, 'displayName') ??
|
|
15
|
+
get(record, 'tracking') ??
|
|
16
|
+
get(record, 'public_id') ??
|
|
17
|
+
getModelName(record)
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@action handleClick() {
|
|
22
|
+
console.log('handleClick called!', ...arguments);
|
|
23
|
+
if (typeof this.args.onClick === 'function') {
|
|
24
|
+
if (this.args.resource) {
|
|
25
|
+
this.args.onClick(this.args.resource, ...arguments);
|
|
26
|
+
} else {
|
|
27
|
+
this.args.onClick(...arguments);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
<div class="query-builder-panel" ...attributes>
|
|
2
2
|
<div class="query-builder-panel-header">
|
|
3
|
-
<div class="
|
|
4
|
-
<
|
|
5
|
-
|
|
3
|
+
<div class="flex flex-row items-center space-x-2">
|
|
4
|
+
<div class="query-builder-panel-title">
|
|
5
|
+
<FaIcon @icon="columns" @size="sm" class="mr-2" />
|
|
6
|
+
Select fields
|
|
7
|
+
</div>
|
|
8
|
+
<Input @value={{this.searchQuery}} class="form-input form-input-sm" placeholder="Search for a column" />
|
|
6
9
|
</div>
|
|
7
10
|
<div class="text-xs text-gray-500">
|
|
8
11
|
{{if this.selectedColumns.length (concat this.selectedColumns.length " selected") "None selected"}}
|
|
@@ -11,7 +14,7 @@
|
|
|
11
14
|
|
|
12
15
|
<div class="query-builder-panel-content no-padding">
|
|
13
16
|
<div class="{{if @columns 'max-h-80 overflow-y-auto' ''}} grid grid-cols-1 gap-2 lg:grid-cols-2 px-2 py-3">
|
|
14
|
-
{{#each
|
|
17
|
+
{{#each this.filteredColumns as |column|}}
|
|
15
18
|
<div class="column-item {{if (includes column.name (map-by 'name' this.selectedColumns)) 'selected'}}">
|
|
16
19
|
<div class="column-checkbox-wrapper">
|
|
17
20
|
<Checkbox @checked={{includes column.name (map-by "name" this.selectedColumns)}} @onToggle={{fn this.selectColumn column}} />
|
|
@@ -5,6 +5,22 @@ import { action } from '@ember/object';
|
|
|
5
5
|
export default class QueryBuilderColumnSelectComponent extends Component {
|
|
6
6
|
@tracked selectedColumns = [];
|
|
7
7
|
@tracked columnAliases = {};
|
|
8
|
+
@tracked searchQuery = '';
|
|
9
|
+
|
|
10
|
+
get filteredColumns() {
|
|
11
|
+
const columns = this.args.columns ?? [];
|
|
12
|
+
const query = (this.searchQuery ?? '').trim().toLowerCase();
|
|
13
|
+
|
|
14
|
+
if (!query) {
|
|
15
|
+
return columns;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return columns.filter((col) => {
|
|
19
|
+
const name = String(col.name ?? '').toLowerCase();
|
|
20
|
+
const label = String(col.label ?? '').toLowerCase();
|
|
21
|
+
return name.includes(query) || label.includes(query);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
8
24
|
|
|
9
25
|
constructor() {
|
|
10
26
|
super(...arguments);
|
|
@@ -12,8 +28,7 @@ export default class QueryBuilderColumnSelectComponent extends Component {
|
|
|
12
28
|
this.columnAliases = this.args.columnAliases || {};
|
|
13
29
|
}
|
|
14
30
|
|
|
15
|
-
@action
|
|
16
|
-
selectColumn(column) {
|
|
31
|
+
@action selectColumn(column) {
|
|
17
32
|
const isSelected = this.selectedColumns.includes(column);
|
|
18
33
|
|
|
19
34
|
if (isSelected) {
|
|
@@ -29,8 +44,7 @@ export default class QueryBuilderColumnSelectComponent extends Component {
|
|
|
29
44
|
this.notifyChange();
|
|
30
45
|
}
|
|
31
46
|
|
|
32
|
-
@action
|
|
33
|
-
updateAlias(columnName, event) {
|
|
47
|
+
@action updateAlias(columnName, event) {
|
|
34
48
|
const aliasValue = event.target.value.trim();
|
|
35
49
|
|
|
36
50
|
if (aliasValue) {
|
|
@@ -48,16 +62,14 @@ export default class QueryBuilderColumnSelectComponent extends Component {
|
|
|
48
62
|
this.notifyChange();
|
|
49
63
|
}
|
|
50
64
|
|
|
51
|
-
@action
|
|
52
|
-
selectAllColumns() {
|
|
65
|
+
@action selectAllColumns() {
|
|
53
66
|
if (this.args.columns) {
|
|
54
67
|
this.selectedColumns = [...this.args.columns];
|
|
55
68
|
this.notifyChange();
|
|
56
69
|
}
|
|
57
70
|
}
|
|
58
71
|
|
|
59
|
-
@action
|
|
60
|
-
clearAllColumns() {
|
|
72
|
+
@action clearAllColumns() {
|
|
61
73
|
this.selectedColumns = [];
|
|
62
74
|
this.columnAliases = {};
|
|
63
75
|
this.notifyChange();
|
|
@@ -379,8 +379,7 @@ export default class QueryBuilderConditionsComponent extends Component {
|
|
|
379
379
|
});
|
|
380
380
|
}
|
|
381
381
|
|
|
382
|
-
@action
|
|
383
|
-
updateGroupOperator(groupIndex, operator) {
|
|
382
|
+
@action updateGroupOperator(groupIndex, operator) {
|
|
384
383
|
// same immutable pattern for the group object
|
|
385
384
|
const groups = [...this.conditionGroups];
|
|
386
385
|
groups[groupIndex] = { ...groups[groupIndex], operator };
|
|
@@ -388,8 +387,7 @@ export default class QueryBuilderConditionsComponent extends Component {
|
|
|
388
387
|
this.notifyChange();
|
|
389
388
|
}
|
|
390
389
|
|
|
391
|
-
@action
|
|
392
|
-
reorderConditionGroups({ sourceList, sourceIndex, targetList, targetIndex }) {
|
|
390
|
+
@action reorderConditionGroups({ sourceList, sourceIndex, targetList, targetIndex }) {
|
|
393
391
|
// no change? bail
|
|
394
392
|
if (sourceList === targetList && sourceIndex === targetIndex) return;
|
|
395
393
|
|
|
@@ -11,10 +11,8 @@
|
|
|
11
11
|
|
|
12
12
|
<div class="query-builder-panel-content">
|
|
13
13
|
{{#if this.canGroup}}
|
|
14
|
-
|
|
15
14
|
<div class="space-y-4">
|
|
16
15
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
17
|
-
|
|
18
16
|
<div>
|
|
19
17
|
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
20
18
|
Group By Column
|
|
@@ -92,7 +90,7 @@
|
|
|
92
90
|
@icon="save"
|
|
93
91
|
@size="xs"
|
|
94
92
|
@onClick={{this.addGroupBy}}
|
|
95
|
-
@disabled={{
|
|
93
|
+
@disabled={{this.isAddGroupingDisabled}}
|
|
96
94
|
/>
|
|
97
95
|
</div>
|
|
98
96
|
|
|
@@ -102,7 +100,7 @@
|
|
|
102
100
|
Active Groupings
|
|
103
101
|
</div>
|
|
104
102
|
|
|
105
|
-
<DragSortList @items={{this.groupByItems}} @
|
|
103
|
+
<DragSortList @items={{this.groupByItems}} @dragEndAction={{this.reorderGroupBy}} class="drag-sort-list" as |item index|>
|
|
106
104
|
<div class="group-sort-item">
|
|
107
105
|
<div class="drag-handle">
|
|
108
106
|
<FaIcon @icon="grip-vertical" @size="sm" />
|
|
@@ -50,22 +50,25 @@ export default class QueryBuilderGroupByComponent extends Component {
|
|
|
50
50
|
get availableAggregateColumns() {
|
|
51
51
|
if (!this.selectedAggregateFn) return [];
|
|
52
52
|
|
|
53
|
-
// Use allSelectedColumns from parent if available
|
|
54
53
|
const columnsToUse = this.args.allSelectedColumns || this.args.selectedColumns || [];
|
|
54
|
+
const fn = this.selectedAggregateFn.value;
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
if (this.selectedAggregateFn.value === 'count') {
|
|
56
|
+
if (fn === 'count') {
|
|
58
57
|
return [{ name: '*', label: 'All Records', type: 'count', full: '*' }, ...columnsToUse];
|
|
59
58
|
}
|
|
60
59
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return columnsToUse.filter((
|
|
60
|
+
if (fn === 'sum' || fn === 'avg') {
|
|
61
|
+
// numeric only
|
|
62
|
+
return columnsToUse.filter((c) => ['integer', 'decimal', 'number', 'float'].includes(c.type));
|
|
64
63
|
}
|
|
65
64
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return columnsToUse.filter((
|
|
65
|
+
if (fn === 'min' || fn === 'max') {
|
|
66
|
+
// numeric + datetime + date + string
|
|
67
|
+
return columnsToUse.filter((c) => ['integer', 'decimal', 'number', 'float', 'date', 'datetime', 'timestamp', 'string', 'text'].includes(c.type));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (fn === 'group_concat') {
|
|
71
|
+
return columnsToUse.filter((c) => ['string', 'text'].includes(c.type));
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
return columnsToUse;
|
|
@@ -93,24 +96,49 @@ export default class QueryBuilderGroupByComponent extends Component {
|
|
|
93
96
|
return null;
|
|
94
97
|
}
|
|
95
98
|
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
get isAddGroupingDisabled() {
|
|
100
|
+
const hasGroupBy = !!this.selectedGroupBy;
|
|
101
|
+
const fn = this.selectedAggregateFn?.value;
|
|
102
|
+
const hasFn = !!fn;
|
|
103
|
+
const hasBy = !!this.selectedAggregateBy;
|
|
104
|
+
|
|
105
|
+
if (!hasGroupBy || !hasFn) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// COUNT requires a selection: either "*" or a column (you can auto-select "*" elsewhere)
|
|
110
|
+
if (fn === 'count') {
|
|
111
|
+
return !hasBy;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// For SUM/AVG/MIN/MAX/GROUP_CONCAT we need:
|
|
115
|
+
// - at least one compatible column available
|
|
116
|
+
// - a selected "aggregate by" column
|
|
117
|
+
const avail = this.availableAggregateColumns ?? [];
|
|
118
|
+
const hasCompatible = avail.length > 0;
|
|
119
|
+
|
|
120
|
+
return !(hasCompatible && hasBy);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@action selectGroupBy(column) {
|
|
98
124
|
this.selectedGroupBy = column;
|
|
99
125
|
}
|
|
100
126
|
|
|
101
|
-
@action
|
|
102
|
-
selectAggregateFn(fn) {
|
|
127
|
+
@action selectAggregateFn(fn) {
|
|
103
128
|
this.selectedAggregateFn = fn;
|
|
104
|
-
|
|
129
|
+
// Optional UX: auto-select "*" when choosing COUNT
|
|
130
|
+
if (fn?.value === 'count') {
|
|
131
|
+
this.selectedAggregateBy = { name: '*', label: 'All Records', type: 'count', full: '*' };
|
|
132
|
+
} else {
|
|
133
|
+
this.selectedAggregateBy = null;
|
|
134
|
+
}
|
|
105
135
|
}
|
|
106
136
|
|
|
107
|
-
@action
|
|
108
|
-
selectAggregateBy(column) {
|
|
137
|
+
@action selectAggregateBy(column) {
|
|
109
138
|
this.selectedAggregateBy = column;
|
|
110
139
|
}
|
|
111
140
|
|
|
112
|
-
@action
|
|
113
|
-
addGroupBy() {
|
|
141
|
+
@action addGroupBy() {
|
|
114
142
|
if (this.selectedGroupBy && this.selectedAggregateFn && this.selectedAggregateBy) {
|
|
115
143
|
// Validate that the groupBy column is actually selected
|
|
116
144
|
const isGroupByColumnSelected = this.args.selectedColumns?.some((col) => col.full === this.selectedGroupBy.full);
|
|
@@ -138,23 +166,35 @@ export default class QueryBuilderGroupByComponent extends Component {
|
|
|
138
166
|
}
|
|
139
167
|
}
|
|
140
168
|
|
|
141
|
-
@action
|
|
142
|
-
removeGroupBy(index) {
|
|
169
|
+
@action removeGroupBy(index) {
|
|
143
170
|
this.groupByItems = this.groupByItems.filter((_, i) => i !== index);
|
|
144
171
|
this.notifyChange();
|
|
145
172
|
}
|
|
146
173
|
|
|
147
|
-
@action
|
|
148
|
-
|
|
149
|
-
|
|
174
|
+
// @action reorderGroupBy(newOrder) {
|
|
175
|
+
// this.groupByItems = newOrder;
|
|
176
|
+
// this.notifyChange();
|
|
177
|
+
// }
|
|
178
|
+
|
|
179
|
+
@action reorderGroupBy({ sourceList, sourceIndex, targetList, targetIndex }) {
|
|
180
|
+
// no change? bail
|
|
181
|
+
if (sourceList === targetList && sourceIndex === targetIndex) return;
|
|
182
|
+
|
|
183
|
+
// mutate the EmberArray in-place (per README)
|
|
184
|
+
const item = sourceList.objectAt(sourceIndex);
|
|
185
|
+
sourceList.removeAt(sourceIndex);
|
|
186
|
+
targetList.insertAt(targetIndex, item);
|
|
187
|
+
|
|
188
|
+
// ensure Glimmer sees a change even if it misses EmberArray observers
|
|
189
|
+
this.groupByItems = [...this.groupByItems];
|
|
190
|
+
|
|
150
191
|
this.notifyChange();
|
|
151
192
|
}
|
|
152
193
|
|
|
153
194
|
/**
|
|
154
195
|
* Validate existing group by items when selected columns change
|
|
155
196
|
*/
|
|
156
|
-
@action
|
|
157
|
-
validateGroupByItems() {
|
|
197
|
+
@action validateGroupByItems() {
|
|
158
198
|
if (!this.args.selectedColumns?.length) {
|
|
159
199
|
// Clear all grouping if no columns selected
|
|
160
200
|
if (this.groupByItems.length > 0) {
|
|
@@ -68,13 +68,6 @@
|
|
|
68
68
|
>
|
|
69
69
|
Select None
|
|
70
70
|
</button>
|
|
71
|
-
<button
|
|
72
|
-
type="button"
|
|
73
|
-
class="text-xs px-2 py-1 bg-green-100 dark:bg-green-800 text-green-700 dark:text-green-300 rounded hover:bg-green-200 dark:hover:bg-green-700"
|
|
74
|
-
{{on "click" (fn this.selectCommonJoinColumns relationship.key)}}
|
|
75
|
-
>
|
|
76
|
-
Common Fields
|
|
77
|
-
</button>
|
|
78
71
|
</div>
|
|
79
72
|
|
|
80
73
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 max-h-48 overflow-y-auto">
|
|
@@ -19,7 +19,6 @@ export default class QueryBuilderJoinsComponent extends Component {
|
|
|
19
19
|
key,
|
|
20
20
|
table: key,
|
|
21
21
|
...relationship,
|
|
22
|
-
columns: this.getColumnsForTable(key),
|
|
23
22
|
};
|
|
24
23
|
});
|
|
25
24
|
}
|
|
@@ -28,64 +27,6 @@ export default class QueryBuilderJoinsComponent extends Component {
|
|
|
28
27
|
return this.joins.reduce((total, join) => total + (join.selectedColumns?.length || 0), 0);
|
|
29
28
|
}
|
|
30
29
|
|
|
31
|
-
getColumnsForTable(tableName) {
|
|
32
|
-
// Get columns from the schema registry or args
|
|
33
|
-
if (this.args.getTableColumns) {
|
|
34
|
-
return this.args.getTableColumns(tableName);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Fallback to mock columns for development
|
|
38
|
-
const mockColumns = {
|
|
39
|
-
drivers: [
|
|
40
|
-
{ name: 'uuid', label: 'ID', type: 'string' },
|
|
41
|
-
{ name: 'name', label: 'Driver Name', type: 'string' },
|
|
42
|
-
{ name: 'email', label: 'Email', type: 'string' },
|
|
43
|
-
{ name: 'phone', label: 'Phone', type: 'string' },
|
|
44
|
-
{ name: 'status', label: 'Status', type: 'string' },
|
|
45
|
-
{ name: 'online', label: 'Online Status', type: 'boolean' },
|
|
46
|
-
{ name: 'created_at', label: 'Created At', type: 'datetime' },
|
|
47
|
-
{ name: 'updated_at', label: 'Updated At', type: 'datetime' },
|
|
48
|
-
],
|
|
49
|
-
vehicles: [
|
|
50
|
-
{ name: 'uuid', label: 'ID', type: 'string' },
|
|
51
|
-
{ name: 'make', label: 'Make', type: 'string' },
|
|
52
|
-
{ name: 'model', label: 'Model', type: 'string' },
|
|
53
|
-
{ name: 'year', label: 'Year', type: 'number' },
|
|
54
|
-
{ name: 'vin', label: 'VIN', type: 'string' },
|
|
55
|
-
{ name: 'plate_number', label: 'Plate Number', type: 'string' },
|
|
56
|
-
{ name: 'status', label: 'Status', type: 'string' },
|
|
57
|
-
{ name: 'created_at', label: 'Created At', type: 'datetime' },
|
|
58
|
-
],
|
|
59
|
-
contacts: [
|
|
60
|
-
{ name: 'uuid', label: 'ID', type: 'string' },
|
|
61
|
-
{ name: 'name', label: 'Contact Name', type: 'string' },
|
|
62
|
-
{ name: 'email', label: 'Email', type: 'string' },
|
|
63
|
-
{ name: 'phone', label: 'Phone', type: 'string' },
|
|
64
|
-
{ name: 'type', label: 'Contact Type', type: 'string' },
|
|
65
|
-
{ name: 'created_at', label: 'Created At', type: 'datetime' },
|
|
66
|
-
],
|
|
67
|
-
places: [
|
|
68
|
-
{ name: 'uuid', label: 'ID', type: 'string' },
|
|
69
|
-
{ name: 'name', label: 'Place Name', type: 'string' },
|
|
70
|
-
{ name: 'street1', label: 'Street Address', type: 'string' },
|
|
71
|
-
{ name: 'city', label: 'City', type: 'string' },
|
|
72
|
-
{ name: 'province', label: 'Province/State', type: 'string' },
|
|
73
|
-
{ name: 'country', label: 'Country', type: 'string' },
|
|
74
|
-
{ name: 'postal_code', label: 'Postal Code', type: 'string' },
|
|
75
|
-
],
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
return mockColumns[tableName] || [];
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
getCommonColumns(tableName) {
|
|
82
|
-
// Return commonly used columns for quick selection
|
|
83
|
-
const commonColumnNames = ['name', 'status', 'email', 'phone', 'created_at'];
|
|
84
|
-
const allColumns = this.getColumnsForTable(tableName);
|
|
85
|
-
|
|
86
|
-
return allColumns.filter((column) => commonColumnNames.includes(column.name) || column.name.includes('name') || column.name.includes('status'));
|
|
87
|
-
}
|
|
88
|
-
|
|
89
30
|
@action
|
|
90
31
|
isJoined(relationshipKey) {
|
|
91
32
|
return this.joins.some((join) => join.key === relationshipKey);
|
|
@@ -186,27 +127,6 @@ export default class QueryBuilderJoinsComponent extends Component {
|
|
|
186
127
|
this.notifyChange();
|
|
187
128
|
}
|
|
188
129
|
|
|
189
|
-
@action
|
|
190
|
-
selectCommonJoinColumns(relationshipKey) {
|
|
191
|
-
const joinIndex = this.joins.findIndex((join) => join.key === relationshipKey);
|
|
192
|
-
if (joinIndex === -1) return;
|
|
193
|
-
|
|
194
|
-
const updatedJoins = [...this.joins];
|
|
195
|
-
const join = { ...updatedJoins[joinIndex] };
|
|
196
|
-
const commonColumns = this.getCommonColumns(relationshipKey);
|
|
197
|
-
|
|
198
|
-
join.selectedColumns = commonColumns.map((column) => ({
|
|
199
|
-
...column,
|
|
200
|
-
table: relationshipKey,
|
|
201
|
-
full: `${relationshipKey}.${column.name}`,
|
|
202
|
-
label: `${join.label} - ${column.label || column.name}`,
|
|
203
|
-
}));
|
|
204
|
-
|
|
205
|
-
updatedJoins[joinIndex] = join;
|
|
206
|
-
this.joins = updatedJoins;
|
|
207
|
-
this.notifyChange();
|
|
208
|
-
}
|
|
209
|
-
|
|
210
130
|
@action
|
|
211
131
|
updateJoinColumnAlias(relationshipKey, columnName, event) {
|
|
212
132
|
const joinIndex = this.joins.findIndex((join) => join.key === relationshipKey);
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import Component from '@glimmer/component';
|
|
2
2
|
import { tracked } from '@glimmer/tracking';
|
|
3
3
|
import { action } from '@ember/object';
|
|
4
|
+
import { next } from '@ember/runloop';
|
|
4
5
|
|
|
5
6
|
export default class QueryBuilderLimitComponent extends Component {
|
|
6
7
|
@tracked limit = null;
|
|
7
8
|
|
|
8
9
|
constructor() {
|
|
9
10
|
super(...arguments);
|
|
10
|
-
this.limit = this.args.limit ||
|
|
11
|
+
this.limit = this.args.limit || 50;
|
|
12
|
+
|
|
13
|
+
// set detault limit
|
|
14
|
+
next(() => this.setLimit(this.limit));
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
get quickLimits() {
|
|
@@ -23,21 +27,18 @@ export default class QueryBuilderLimitComponent extends Component {
|
|
|
23
27
|
];
|
|
24
28
|
}
|
|
25
29
|
|
|
26
|
-
@action
|
|
27
|
-
setLimit(value) {
|
|
30
|
+
@action setLimit(value) {
|
|
28
31
|
const numValue = parseInt(value, 10);
|
|
29
32
|
this.limit = isNaN(numValue) || numValue <= 0 ? null : numValue;
|
|
30
33
|
this.notifyChange();
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
@action
|
|
34
|
-
setQuickLimit(value) {
|
|
36
|
+
@action setQuickLimit(value) {
|
|
35
37
|
this.limit = value;
|
|
36
38
|
this.notifyChange();
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
@action
|
|
40
|
-
clearLimit() {
|
|
41
|
+
@action clearLimit() {
|
|
41
42
|
this.limit = null;
|
|
42
43
|
this.notifyChange();
|
|
43
44
|
}
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
Sort Order (drag to reorder)
|
|
69
69
|
</div>
|
|
70
70
|
|
|
71
|
-
<DragSortList @items={{this.sortByItems}} @
|
|
71
|
+
<DragSortList @items={{this.sortByItems}} @dragEndAction={{this.reorderSortBy}} class="drag-sort-list" as |item index|>
|
|
72
72
|
<div class="group-sort-item">
|
|
73
73
|
<div class="drag-handle">
|
|
74
74
|
<FaIcon @icon="grip-vertical" @size="sm" />
|
|
@@ -62,18 +62,15 @@ export default class QueryBuilderSortByComponent extends Component {
|
|
|
62
62
|
return null;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
@action
|
|
66
|
-
selectSortColumn(column) {
|
|
65
|
+
@action selectSortColumn(column) {
|
|
67
66
|
this.selectedSortBy = column;
|
|
68
67
|
}
|
|
69
68
|
|
|
70
|
-
@action
|
|
71
|
-
selectSortDirection(direction) {
|
|
69
|
+
@action selectSortDirection(direction) {
|
|
72
70
|
this.selectedSortDirection = direction;
|
|
73
71
|
}
|
|
74
72
|
|
|
75
|
-
@action
|
|
76
|
-
addSortBy() {
|
|
73
|
+
@action addSortBy() {
|
|
77
74
|
if (this.selectedSortBy && this.selectedSortDirection) {
|
|
78
75
|
// Validate that the sort column is actually selected
|
|
79
76
|
const isSortColumnSelected = this.args.selectedColumns?.some((col) => col.full === this.selectedSortBy.full);
|
|
@@ -113,20 +110,32 @@ export default class QueryBuilderSortByComponent extends Component {
|
|
|
113
110
|
}
|
|
114
111
|
}
|
|
115
112
|
|
|
116
|
-
@action
|
|
117
|
-
removeSortBy(index) {
|
|
113
|
+
@action removeSortBy(index) {
|
|
118
114
|
this.sortByItems = this.sortByItems.filter((_, i) => i !== index);
|
|
119
115
|
this.notifyChange();
|
|
120
116
|
}
|
|
121
117
|
|
|
122
|
-
@action
|
|
123
|
-
|
|
124
|
-
|
|
118
|
+
// @action reorderSortBy(newOrder) {
|
|
119
|
+
// this.sortByItems = newOrder;
|
|
120
|
+
// this.notifyChange();
|
|
121
|
+
// }
|
|
122
|
+
|
|
123
|
+
@action reorderGroupBy({ sourceList, sourceIndex, targetList, targetIndex }) {
|
|
124
|
+
// no change? bail
|
|
125
|
+
if (sourceList === targetList && sourceIndex === targetIndex) return;
|
|
126
|
+
|
|
127
|
+
// mutate the EmberArray in-place (per README)
|
|
128
|
+
const item = sourceList.objectAt(sourceIndex);
|
|
129
|
+
sourceList.removeAt(sourceIndex);
|
|
130
|
+
targetList.insertAt(targetIndex, item);
|
|
131
|
+
|
|
132
|
+
// ensure Glimmer sees a change even if it misses EmberArray observers
|
|
133
|
+
this.sortByItems = [...this.sortByItems];
|
|
134
|
+
|
|
125
135
|
this.notifyChange();
|
|
126
136
|
}
|
|
127
137
|
|
|
128
|
-
@action
|
|
129
|
-
toggleSortDirection(index) {
|
|
138
|
+
@action toggleSortDirection(index) {
|
|
130
139
|
const updatedItems = [...this.sortByItems];
|
|
131
140
|
const currentDirection = updatedItems[index].direction.value;
|
|
132
141
|
const newDirection = currentDirection === 'asc' ? this.directions[1] : this.directions[0];
|
|
@@ -143,8 +152,7 @@ export default class QueryBuilderSortByComponent extends Component {
|
|
|
143
152
|
/**
|
|
144
153
|
* Validate existing sort items when selected columns change
|
|
145
154
|
*/
|
|
146
|
-
@action
|
|
147
|
-
validateSortItems() {
|
|
155
|
+
@action validateSortItems() {
|
|
148
156
|
const columnsToUse = this.args.allSelectedColumns || this.args.selectedColumns || [];
|
|
149
157
|
|
|
150
158
|
if (!columnsToUse.length) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div class="details-wrapper" ...attributes>
|
|
2
2
|
<ContentPanel @title="Report Details" @open={{true}} @wrapperClass="bordered-top">
|
|
3
|
-
<div class="grid grid-cols-2 gap-2 text-xs dark:text-gray-100
|
|
3
|
+
<div class="grid grid-cols-2 gap-2 text-xs dark:text-gray-100">
|
|
4
4
|
<div class="field-info-container">
|
|
5
5
|
<div class="field-name">Title</div>
|
|
6
6
|
<div class="field-value">{{n-a @resource.title}}</div>
|
|
@@ -9,12 +9,6 @@
|
|
|
9
9
|
<div class="field-name">Description</div>
|
|
10
10
|
<div class="field-value">{{n-a @resource.description}}</div>
|
|
11
11
|
</div>
|
|
12
|
-
<div class="field-info-container">
|
|
13
|
-
<div class="field-name">Status</div>
|
|
14
|
-
<div class="field-value">
|
|
15
|
-
<Badge @status={{@resource.status}} />
|
|
16
|
-
</div>
|
|
17
|
-
</div>
|
|
18
12
|
<div class="field-info-container">
|
|
19
13
|
<div class="field-name">Date Created</div>
|
|
20
14
|
<div class="field-value">{{@resource.createdAt}}</div>
|
|
@@ -24,6 +18,8 @@
|
|
|
24
18
|
<div class="field-value flex flex-row items-center space-x-1">
|
|
25
19
|
{{#each @resource.tags as |tag|}}
|
|
26
20
|
<div class="rounded-xl border text-sm border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-2 py-0.5 flex items-center justify-center">{{tag}}</div>
|
|
21
|
+
{{else}}
|
|
22
|
+
<div>-</div>
|
|
27
23
|
{{/each}}
|
|
28
24
|
</div>
|
|
29
25
|
</div>
|
|
@@ -218,7 +218,7 @@ export default class UnitInputComponent extends Component {
|
|
|
218
218
|
* @memberof UnitInputComponent
|
|
219
219
|
*/
|
|
220
220
|
@computed('selectedUnitObject.name', 'unit') get unitName() {
|
|
221
|
-
return this.selectedUnitObject
|
|
221
|
+
return this.selectedUnitObject?.name ?? this.unit;
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
/**
|
|
@@ -53,10 +53,10 @@ export default class DashboardService extends Service {
|
|
|
53
53
|
/**
|
|
54
54
|
* Task for loading dashboards from the store. It sets the current dashboard and checks if adding widget is necessary.
|
|
55
55
|
*/
|
|
56
|
-
@task *loadDashboards(defaultDashboardId = 'dashboard', defaultDashboardName = 'Default Dashboard') {
|
|
56
|
+
@task *loadDashboards({ defaultDashboardId = 'dashboard', defaultDashboardName = 'Default Dashboard', extension = 'core' }) {
|
|
57
57
|
this.universe.registerDashboard(defaultDashboardId);
|
|
58
58
|
|
|
59
|
-
const dashboards = yield this.store.
|
|
59
|
+
const dashboards = yield this.store.query('dashboard', { limit: -1, extension });
|
|
60
60
|
if (isArray(dashboards)) {
|
|
61
61
|
this.dashboards = typeof dashboards.toArray === 'function' ? dashboards.toArray() : dashboards;
|
|
62
62
|
|
|
@@ -98,16 +98,18 @@ export default class DashboardService extends Service {
|
|
|
98
98
|
* Task for creating a new dashboard. It handles dashboard creation, success notification, and dashboard selection.
|
|
99
99
|
* @param {string} name - Name of the new dashboard.
|
|
100
100
|
*/
|
|
101
|
-
@task *createDashboard(name) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
101
|
+
@task *createDashboard(name, attributes = {}) {
|
|
102
|
+
try {
|
|
103
|
+
const dashboardRecord = this.store.createRecord('dashboard', { name, is_default: true, ...attributes });
|
|
104
|
+
const dashboard = yield dashboardRecord.save();
|
|
105
|
+
|
|
106
|
+
if (dashboard) {
|
|
107
|
+
this.notifications.success(this.intl.t('services.dashboard-service.create-dashboard-success-notification', { dashboardName: dashboard.name }));
|
|
108
|
+
this.selectDashboard.perform(dashboard);
|
|
109
|
+
this.dashboards.pushObject(dashboard);
|
|
110
|
+
}
|
|
111
|
+
} catch (err) {
|
|
112
|
+
this.notifications.serverError(err);
|
|
111
113
|
}
|
|
112
114
|
}
|
|
113
115
|
|
|
@@ -79,12 +79,14 @@
|
|
|
79
79
|
|
|
80
80
|
.status-badge.ready-status-badge > span,
|
|
81
81
|
.status-badge.verified-status-badge > span,
|
|
82
|
+
.status-badge.allocated-status-badge > span,
|
|
82
83
|
.status-badge.emerald-status-badge > span {
|
|
83
84
|
@apply bg-emerald-800 border-emerald-700 text-emerald-100;
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
.status-badge.ready-status-badge > span svg,
|
|
87
88
|
.status-badge.verified-status-badge > span svg,
|
|
89
|
+
.status-badge.allocated-status-badge > span svg,
|
|
88
90
|
.status-badge.emerald-status-badge > span svg {
|
|
89
91
|
@apply text-emerald-300;
|
|
90
92
|
}
|
|
@@ -123,16 +125,21 @@
|
|
|
123
125
|
@apply text-slate-300;
|
|
124
126
|
}
|
|
125
127
|
|
|
128
|
+
.status-badge.disabled-status-badge > span,
|
|
126
129
|
.status-badge.gray-status-badge > span {
|
|
127
130
|
@apply bg-gray-800 border-gray-700 text-gray-100;
|
|
128
131
|
}
|
|
129
132
|
|
|
133
|
+
.status-badge.disabled-status-badge > span svg,
|
|
130
134
|
.status-badge.gray-status-badge > span svg {
|
|
131
135
|
@apply text-gray-300;
|
|
132
136
|
}
|
|
133
137
|
|
|
134
138
|
.status-badge[class*='5'] > span,
|
|
135
139
|
.status-badge.red-status-badge > span,
|
|
140
|
+
.status-badge.faulty-status-badge > span,
|
|
141
|
+
.status-badge.degraded-status-badge > span,
|
|
142
|
+
.status-badge.error-status-badge > span,
|
|
136
143
|
.status-badge.escalated-status-badge > span,
|
|
137
144
|
.status-badge.high-status-badge > span,
|
|
138
145
|
.status-badge.rejected-status-badge > span,
|
|
@@ -147,6 +154,9 @@
|
|
|
147
154
|
|
|
148
155
|
.status-badge[class*='5'] > span svg,
|
|
149
156
|
.status-badge.red-status-badge > span svg,
|
|
157
|
+
.status-badge.faulty-status-badge > span svg,
|
|
158
|
+
.status-badge.degraded-status-badge > span svg,
|
|
159
|
+
.status-badge.error-status-badge > span svg,
|
|
150
160
|
.status-badge.escalated-status-badge > span svg,
|
|
151
161
|
.status-badge.high-status-badge > span svg,
|
|
152
162
|
.status-badge.rejected-status-badge > span svg,
|
|
@@ -161,6 +171,7 @@
|
|
|
161
171
|
}
|
|
162
172
|
|
|
163
173
|
.status-badge[class*='4'] > span,
|
|
174
|
+
.status-badge.idle-status-badge > span,
|
|
164
175
|
.status-badge.draft-status-badge > span,
|
|
165
176
|
.status-badge.yellow-status-badge > span,
|
|
166
177
|
.status-badge.re-opened-status-badge > span,
|
|
@@ -168,8 +179,10 @@
|
|
|
168
179
|
.status-badge.driver-enroute-status-badge > span,
|
|
169
180
|
.status-badge.offline-status-badge > span,
|
|
170
181
|
.status-badge.pending-review-status-badge > span,
|
|
182
|
+
.status-badge.pending-activation-status-badge > span,
|
|
171
183
|
.status-badge.awaiting-review-status-badge > span,
|
|
172
184
|
.status-badge.scheduled-maintenance-status-badge > span,
|
|
185
|
+
.status-badge.maintenance-status-badge > span,
|
|
173
186
|
.status-badge.test-status-badge > span,
|
|
174
187
|
.status-badge.warning-status-badge > span,
|
|
175
188
|
.status-badge.preparing-status-badge > span,
|
|
@@ -181,6 +194,7 @@
|
|
|
181
194
|
}
|
|
182
195
|
|
|
183
196
|
.status-badge[class*='4'] > span svg,
|
|
197
|
+
.status-badge.idle-status-badge > span svg,
|
|
184
198
|
.status-badge.draft-status-badge > span svg,
|
|
185
199
|
.status-badge.yellow-status-badge > span svg,
|
|
186
200
|
.status-badge.re-opened-status-badge > span svg,
|
|
@@ -188,8 +202,10 @@
|
|
|
188
202
|
.status-badge.driver-enroute-status-badge > span svg,
|
|
189
203
|
.status-badge.offline-status-badge > span svg,
|
|
190
204
|
.status-badge.pending-review-status-badge > span svg,
|
|
205
|
+
.status-badge.pending-activation-status-badge > span svg,
|
|
191
206
|
.status-badge.awaiting-review-status-badge > span svg,
|
|
192
207
|
.status-badge.scheduled-maintenance-status-badge > span svg,
|
|
208
|
+
.status-badge.maintenance-status-badge > span svg,
|
|
193
209
|
.status-badge.test-status-badge > span svg,
|
|
194
210
|
.status-badge.warning-status-badge > span svg,
|
|
195
211
|
.status-badge.preparing-status-badge > span svg,
|
|
@@ -201,6 +217,7 @@
|
|
|
201
217
|
}
|
|
202
218
|
|
|
203
219
|
.status-badge.indigo-status-badge > span,
|
|
220
|
+
.status-badge.initialized-status-badge > span,
|
|
204
221
|
.status-badge.operational-suggestion-status-badge > span,
|
|
205
222
|
.status-badge.dispatched-status-badge > span,
|
|
206
223
|
.status-badge.matched-status-badge > span,
|
|
@@ -209,6 +226,7 @@
|
|
|
209
226
|
}
|
|
210
227
|
|
|
211
228
|
.status-badge.indigo-status-badge > span svg,
|
|
229
|
+
.status-badge.initialized-status-badge > span svg,
|
|
212
230
|
.status-badge.operational-suggestion-status-badge > span svg,
|
|
213
231
|
.status-badge.dispatched-status-badge > span svg,
|
|
214
232
|
.status-badge.matched-status-badge > span svg,
|
|
@@ -217,6 +235,9 @@
|
|
|
217
235
|
}
|
|
218
236
|
|
|
219
237
|
.status-badge.orange-status-badge > span,
|
|
238
|
+
.status-badge.decommissioned-status-badge > span,
|
|
239
|
+
.status-badge.disconnected-status-badge > span,
|
|
240
|
+
.status-badge.calibrating-status-badge > span,
|
|
220
241
|
.status-badge.awaiting-parts-status-badge > span,
|
|
221
242
|
.status-badge.monitoring-status-badge > span,
|
|
222
243
|
.status-badge.triage-status-badge > span,
|
|
@@ -227,6 +248,9 @@
|
|
|
227
248
|
}
|
|
228
249
|
|
|
229
250
|
.status-badge.orange-status-badge > span svg,
|
|
251
|
+
.status-badge.decommissioned-status-badge > span svg,
|
|
252
|
+
.status-badge.disconnected-status-badge > span svg,
|
|
253
|
+
.status-badge.calibrating-status-badge > span svg,
|
|
230
254
|
.status-badge.awaiting-parts-status-badge > span svg,
|
|
231
255
|
.status-badge.monitoring-status-badge > span svg,
|
|
232
256
|
.status-badge.triage-status-badge > span svg,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from '@fleetbase/ember-ui/components/pill';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from '@fleetbase/ember-ui/helpers/is-object-empty';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fleetbase/ember-ui",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"description": "Fleetbase UI provides all the interface components, helpers, services and utilities for building a Fleetbase extension into the Console.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fleetbase-ui",
|