mirror of
https://github.com/fleetbase/fleetbase.git
synced 2026-01-08 07:16:49 +00:00
Merge pull request #197 from fleetbase/dashboard
customizable widget based dashboard
This commit is contained in:
26
api/composer.lock
generated
26
api/composer.lock
generated
@@ -192,16 +192,16 @@
|
||||
},
|
||||
{
|
||||
"name": "aws/aws-sdk-php",
|
||||
"version": "3.298.0",
|
||||
"version": "3.298.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/aws/aws-sdk-php.git",
|
||||
"reference": "55536f81006d8721c51e342d638e7ccc3529e754"
|
||||
"reference": "7c7dd6f596e7f7ba22653a503ae76e8e11702028"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/55536f81006d8721c51e342d638e7ccc3529e754",
|
||||
"reference": "55536f81006d8721c51e342d638e7ccc3529e754",
|
||||
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/7c7dd6f596e7f7ba22653a503ae76e8e11702028",
|
||||
"reference": "7c7dd6f596e7f7ba22653a503ae76e8e11702028",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -281,9 +281,9 @@
|
||||
"support": {
|
||||
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
|
||||
"issues": "https://github.com/aws/aws-sdk-php/issues",
|
||||
"source": "https://github.com/aws/aws-sdk-php/tree/3.298.0"
|
||||
"source": "https://github.com/aws/aws-sdk-php/tree/3.298.1"
|
||||
},
|
||||
"time": "2024-01-31T19:06:05+00:00"
|
||||
"time": "2024-02-01T19:07:41+00:00"
|
||||
},
|
||||
{
|
||||
"name": "aws/aws-sdk-php-laravel",
|
||||
@@ -2935,21 +2935,21 @@
|
||||
},
|
||||
{
|
||||
"name": "google/auth",
|
||||
"version": "v1.34.0",
|
||||
"version": "v1.35.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/googleapis/google-auth-library-php.git",
|
||||
"reference": "155daeadfd2f09743f611ea493b828d382519575"
|
||||
"reference": "6e9c9fd4e2bbd7042f50083076346e4a1eff4e4b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/googleapis/google-auth-library-php/zipball/155daeadfd2f09743f611ea493b828d382519575",
|
||||
"reference": "155daeadfd2f09743f611ea493b828d382519575",
|
||||
"url": "https://api.github.com/repos/googleapis/google-auth-library-php/zipball/6e9c9fd4e2bbd7042f50083076346e4a1eff4e4b",
|
||||
"reference": "6e9c9fd4e2bbd7042f50083076346e4a1eff4e4b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"firebase/php-jwt": "^6.0",
|
||||
"guzzlehttp/guzzle": "^6.2.1|^7.0",
|
||||
"guzzlehttp/guzzle": "^6.5.8||^7.4.5",
|
||||
"guzzlehttp/psr7": "^2.4.5",
|
||||
"php": "^7.4||^8.0",
|
||||
"psr/cache": "^1.0||^2.0||^3.0",
|
||||
@@ -2987,9 +2987,9 @@
|
||||
"support": {
|
||||
"docs": "https://googleapis.github.io/google-auth-library-php/main/",
|
||||
"issues": "https://github.com/googleapis/google-auth-library-php/issues",
|
||||
"source": "https://github.com/googleapis/google-auth-library-php/tree/v1.34.0"
|
||||
"source": "https://github.com/googleapis/google-auth-library-php/tree/v1.35.0"
|
||||
},
|
||||
"time": "2024-01-03T20:45:15+00:00"
|
||||
"time": "2024-02-01T20:41:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "google/cloud-core",
|
||||
|
||||
@@ -1,7 +1,95 @@
|
||||
<div class="fleetbase-dashboard-grid">
|
||||
<div class="grid grid-cols-1 md:grid-cols-12 gap-4">
|
||||
{{#each this.dashboards as |dashboard index|}}
|
||||
<Dashboard::Create @index={{index}} @dashboard={{dashboard}} />
|
||||
{{/each}}
|
||||
<div class="fleetbase-dashboard-grid flex items-center justify-between mb-4 mt-6 px-14">
|
||||
<div class="left-section">
|
||||
{{! Dashboard Title Text }}
|
||||
<h1 class="text-lg font-bold">{{this.dashboard.currentDashboard.name}}</h1>
|
||||
</div>
|
||||
<div class="fleetbase-dashboard-actions right-section ml-4 flex items-center">
|
||||
{{! Select Dropdown }}
|
||||
<div class="fleetbase-model-select fleetbase-power-select ember-model-select h-10">
|
||||
|
||||
<DropdownButton
|
||||
class="h-10"
|
||||
@text={{if this.dashboard.currentDashboard.name this.dashboard.currentDashboard.name "Select Dashboard"}}
|
||||
@textClass="text-sm mr-2"
|
||||
@buttonClass="flex-row-reverse"
|
||||
@icon="caret-down"
|
||||
@iconClass="mr-0i"
|
||||
@size="sm"
|
||||
@iconPrefix="fas"
|
||||
@triggerClass="hidden md:flex"
|
||||
as |dd|
|
||||
>
|
||||
<div class="next-dd-menu mt-1 mx-0" aria-labelledby="user-menu">
|
||||
<div class="p-1">
|
||||
{{#each this.dashboard.dashboards as |dashboard|}}
|
||||
<a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.selectDashboard dashboard)}}>
|
||||
<div class="w-6">
|
||||
<FaIcon @icon="desktop" />
|
||||
</div>
|
||||
<span>{{dashboard.name}}</span>
|
||||
</a>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</DropdownButton>
|
||||
</div>
|
||||
{{! Button }}
|
||||
|
||||
<div class="ml-2 relative h-10">
|
||||
<DropdownButton class="h-10" @icon="ellipsis-h" @size="sm" @iconPrefix="fas" @triggerClass="hidden md:flex" as |dd|>
|
||||
<div class="next-dd-menu mt-1 mx-0" aria-labelledby="user-menu">
|
||||
<div class="p-1">
|
||||
<a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.createDashboard)}}>
|
||||
<div class="w-6">
|
||||
<FaIcon @icon="add" />
|
||||
</div>
|
||||
<span>Create new Dashboard</span>
|
||||
</a>
|
||||
|
||||
{{#unless (eq this.dashboard.currentDashboard.owner_uuid "system")}}
|
||||
<a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.onChangeEdit true)}}>
|
||||
<div class="w-6">
|
||||
<FaIcon @icon="edit" />
|
||||
</div>
|
||||
<span>Edit layout</span>
|
||||
</a>
|
||||
<a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.onAddingWidget true)}}>
|
||||
<div class="w-6">
|
||||
<FaIcon @icon="add" />
|
||||
</div>
|
||||
<span>Add widgets</span>
|
||||
</a>
|
||||
|
||||
<a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.deleteDashboard this.dashboard.currentDashboard)}}>
|
||||
<div class="w-6">
|
||||
<FaIcon @icon="trash" />
|
||||
</div>
|
||||
<span>Delete dashboard</span>
|
||||
</a>
|
||||
{{/unless}}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</DropdownButton>
|
||||
</div>
|
||||
{{#if this.dashboard.isEditingDashboard}}
|
||||
<div class="ml-2 h-10">
|
||||
<Button @type="magic" @icon="save" @helpText={{"Save Dashboard"}} @onClick={{fn this.onChangeEdit false}} class="h-10" />
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-10">
|
||||
<Dashboard::Create @isEdit={{this.dashboard.isEditingDashboard}} @isAddingWidget={{this.dashboard.isAddingWidget}} @dashboard={{this.dashboard.currentDashboard}} />
|
||||
{{#if this.dashboard.isAddingWidget}}
|
||||
<EmberWormhole @to="console-home-wormhole">
|
||||
<Dashboard::WidgetPanel
|
||||
@isOpen={{this.dashboard.isAddingWidget}}
|
||||
@onLoad={{this.setWidgetSelectorPanelContext}}
|
||||
@dashboard={{this.dashboard.currentDashboard}}
|
||||
@onClose={{fn this.onAddingWidget false}}
|
||||
/>
|
||||
</EmberWormhole>
|
||||
{{/if}}
|
||||
</div>
|
||||
@@ -1,48 +1,84 @@
|
||||
import Component from '@glimmer/component';
|
||||
import loadExtensions from '@fleetbase/ember-core/utils/load-extensions';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { isArray } from '@ember/array';
|
||||
import { inject as service } from '@ember/service';
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { task } from 'ember-concurrency-decorators';
|
||||
|
||||
export default class DashboardComponent extends Component {
|
||||
@service store;
|
||||
@service notifications;
|
||||
@service modalsManager;
|
||||
@service fetch;
|
||||
@tracked extensions;
|
||||
@tracked dashboards = [];
|
||||
@tracked isLoading;
|
||||
@service dashboard;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.loadExtensions();
|
||||
this.loadDashboards.perform();
|
||||
}
|
||||
|
||||
@action async loadExtensions() {
|
||||
this.extensions = await loadExtensions();
|
||||
this.loadDashboardBuilds.perform();
|
||||
@task *loadDashboards() {
|
||||
this.dashboard.loadDashboards.perform();
|
||||
}
|
||||
|
||||
@task *loadDashboard(extension) {
|
||||
this.isLoading = extension.extension;
|
||||
let dashboardBuild;
|
||||
|
||||
try {
|
||||
dashboardBuild = yield this.fetch.get(extension.fleetbase.dashboard, {}, { namespace: '' });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isArray(dashboardBuild)) {
|
||||
this.dashboards = [...this.dashboards, ...dashboardBuild.map((build) => ({ ...build, extension }))];
|
||||
}
|
||||
@action selectDashboard(dashboard) {
|
||||
this.dashboard.selectDashboard.perform(dashboard);
|
||||
}
|
||||
|
||||
@task({ enqueue: true, maxConcurrency: 1 }) *loadDashboardBuilds() {
|
||||
const extensionsWithDashboards = this.extensions.filter((extension) => typeof extension.fleetbase?.dashboard === 'string');
|
||||
@action setWidgetSelectorPanelContext(widgetSelectorContext) {
|
||||
this.widgetSelectorContext = widgetSelectorContext;
|
||||
}
|
||||
|
||||
for (let i = 0; i < extensionsWithDashboards.length; i++) {
|
||||
const extension = extensionsWithDashboards[i];
|
||||
yield this.loadDashboard.perform(extension);
|
||||
@action onChangeEdit(state = true) {
|
||||
this.isEditingDashboard = state;
|
||||
}
|
||||
|
||||
@action createDashboard(dashboard, options = {}) {
|
||||
this.modalsManager.show('modals/create-dashboard', {
|
||||
title: `Create a new dashboard`,
|
||||
acceptButtonText: 'Save Changes',
|
||||
confirm: async (modal, done) => {
|
||||
modal.startLoading();
|
||||
|
||||
// Get the name from the modal options
|
||||
const { name } = modal.getOptions();
|
||||
|
||||
await this.dashboard.createDashboard.perform(name);
|
||||
done();
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
@action deleteDashboard(dashboard, options = {}) {
|
||||
if (this.dashboard.dashboards?.length === 1) {
|
||||
return this.notifications.error('You cannot delete the last dashboard.');
|
||||
}
|
||||
|
||||
this.modalsManager.confirm({
|
||||
title: `Are you sure to delete this ${dashboard.name}?`,
|
||||
confirm: async (modal, done) => {
|
||||
if (typeof options.onConfirm === 'function') {
|
||||
options.onConfirm(model);
|
||||
}
|
||||
|
||||
modal.startLoading();
|
||||
|
||||
await this.dashboard.deleteDashboard.perform(dashboard);
|
||||
done();
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
setCurrentDashboard(dashboard) {
|
||||
this.dashboard.setCurrentDashboard.perform(dashboard);
|
||||
}
|
||||
|
||||
onChangeEdit(state = true) {
|
||||
this.dashboard.onChangeEdit(state);
|
||||
}
|
||||
|
||||
@action onAddingWidget(state = true) {
|
||||
this.dashboard.onAddingWidget(state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="dashboard-component-count lg:col-span-2">
|
||||
<div class="dashboard-component-count lg:col-span-2 h-full">
|
||||
<h3 class="text-sm dark:text-gray-100 text-black mb-4">{{@options.title}}</h3>
|
||||
<h1 class="text-3xl font-bold dark:text-gray-100 text-black mb-4">
|
||||
{{this.displayValue}}
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
<div class="col-span-{{or @dashboard.size 12}}">
|
||||
<div class="dashboard-title flex flex-col lg:flex-row lg:items-center">
|
||||
<div class="flex flex-row items-center mb-2 lg:mb-0">
|
||||
{{#if this.isLoading}}
|
||||
<Spinner class="mr-2i" />
|
||||
{{/if}}
|
||||
<h2 class="text-sm font-bold dark:text-gray-100 text-black">{{@dashboard.title}}</h2>
|
||||
</div>
|
||||
<div>
|
||||
<Dashboard::QueryParams @params={{@dashboard.queryParams}} @onChange={{this.onQueryParamsChanged}} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 lg:grid-cols-12 gap-4">
|
||||
{{#each this.dashboard.widgets as |widget|}}
|
||||
{{component (concat "dashboard/" widget.component) options=widget.options}}
|
||||
<div class="fleetbase-dashboard-grid" ...attributes>
|
||||
<GridStack @options={{this.gridOptions}} @onChange={{this.onChangeGrid}}>
|
||||
{{#each @dashboard.widgets as |widget|}}
|
||||
<GridStackItem @options={{spread-widget-options (hash id=widget.uuid options=widget.grid_options)}} class="relative">
|
||||
{{component widget.component options=widget.options}}
|
||||
{{#if @isEdit}}
|
||||
<div class="absolute top-2 right-2">
|
||||
<Button @type="default" @icon="trash" @helpText={{"Remove widget from the dashboard"}} @onClick={{fn this.removeWidget widget}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
</GridStackItem>
|
||||
{{/each}}
|
||||
</div>
|
||||
</GridStack>
|
||||
</div>
|
||||
@@ -1,41 +1,67 @@
|
||||
import { action, computed } from '@ember/object';
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
import { merge } from '@ember/object/internals';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { isArray } from '@ember/array';
|
||||
import { task } from 'ember-concurrency-decorators';
|
||||
|
||||
export default class DashboardCreateComponent extends Component {
|
||||
@service fetch;
|
||||
@tracked isLoading = false;
|
||||
@tracked dashboard;
|
||||
@service notifications;
|
||||
|
||||
constructor() {
|
||||
constructor(owner, args) {
|
||||
super(...arguments);
|
||||
this.dashboard = this.args.dashboard;
|
||||
}
|
||||
|
||||
@action onQueryParamsChanged(changedParams) {
|
||||
this.reloadDashboard.perform(changedParams);
|
||||
@action
|
||||
toggleFloat() {
|
||||
this.shouldFloat = !this.shouldFloat;
|
||||
}
|
||||
|
||||
@task *reloadDashboard(params) {
|
||||
const { extension } = this.args.dashboard;
|
||||
const index = this.args.index;
|
||||
let dashboards = [];
|
||||
@action onChangeGrid(event) {
|
||||
console.log('Grid Stack event: ', event);
|
||||
console.log(
|
||||
'dashboard: ',
|
||||
this.args.dashboard.widgets.map((widget) => widget.serialize())
|
||||
);
|
||||
|
||||
this.isLoading = true;
|
||||
const updatedWidgets = [];
|
||||
|
||||
try {
|
||||
dashboards = yield this.fetch.get(extension.fleetbase.dashboard, params, { namespace: '' });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
event.detail.forEach((currentWidgetEvent, index) => {
|
||||
const alreadyUpdated = updatedWidgets.find((item) => item.uuid === currentWidgetEvent.id);
|
||||
if (alreadyUpdated) {
|
||||
return; // Skip updating if already updated
|
||||
}
|
||||
|
||||
this.isLoading = false;
|
||||
const changedWidget = this.args.dashboard.widgets.find((widget) => widget.id === currentWidgetEvent.id);
|
||||
if (!changedWidget) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isArray(dashboards)) {
|
||||
this.dashboard = dashboards.objectAt(index);
|
||||
}
|
||||
const { id, x, y, w, h } = currentWidgetEvent;
|
||||
changedWidget.grid_options = { x, y, w, h };
|
||||
const response = changedWidget.updatePosition({ id, x, y, h, w });
|
||||
if (response) {
|
||||
updatedWidgets.push(changedWidget);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@action removeWidget(widget) {
|
||||
this.args.dashboard.removeWidget(widget.id).catch((error) => {
|
||||
this.notifications.serverError(error);
|
||||
});
|
||||
}
|
||||
|
||||
@computed('args.isEdit')
|
||||
get gridOptions() {
|
||||
return {
|
||||
float: true,
|
||||
animate: true,
|
||||
acceptWidgets: true,
|
||||
alwaysShowResizeHandle: this.args.isEdit,
|
||||
disableDrag: !this.args.isEdit,
|
||||
disableResize: !this.args.isEdit,
|
||||
resizable: { handles: 'all' },
|
||||
cellHeight: 30,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
18
console/app/components/dashboard/metric.hbs
Normal file
18
console/app/components/dashboard/metric.hbs
Normal file
@@ -0,0 +1,18 @@
|
||||
<div class="col-span-{{or @dashboard.size 12}}">
|
||||
<div class="dashboard-title flex flex-col lg:flex-row lg:items-center">
|
||||
<div class="flex flex-row items-center mb-2 lg:mb-0">
|
||||
{{#if this.isLoading}}
|
||||
<Spinner class="mr-2i" />
|
||||
{{/if}}
|
||||
<h2 class="text-sm font-bold dark:text-gray-100 text-black">{{this.dashboard.title}}</h2>
|
||||
</div>
|
||||
<div>
|
||||
<Dashboard::QueryParams @params={{this.dashboard.queryParams}} @onChange={{this.onQueryParamsChanged}} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 lg:grid-cols-12 gap-4">
|
||||
{{#each this.dashboard.widgets as |widget|}}
|
||||
{{component (concat "dashboard/" widget.component) options=widget.options}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
41
console/app/components/dashboard/metric.js
Normal file
41
console/app/components/dashboard/metric.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { isArray } from '@ember/array';
|
||||
import { task } from 'ember-concurrency-decorators';
|
||||
import { ar } from 'date-fns/locale';
|
||||
|
||||
export default class MetricComponent extends Component {
|
||||
@service fetch;
|
||||
@tracked isLoading = false;
|
||||
@tracked dashboard;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
console.log('Dashboard in Metric: ', this.args.options);
|
||||
this.loadDashboard.perform();
|
||||
}
|
||||
|
||||
@action onQueryParamsChanged(changedParams) {
|
||||
this.loadDashboard.perform(changedParams);
|
||||
}
|
||||
|
||||
@task *loadDashboard(params) {
|
||||
let dashboards = [];
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
try {
|
||||
dashboards = yield this.fetch.get(this.args.options.endpoint, params, { namespace: '' });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoading = false;
|
||||
|
||||
if (isArray(dashboards)) {
|
||||
this.dashboard = dashboards.objectAt(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
32
console/app/components/dashboard/widget-panel.hbs
Normal file
32
console/app/components/dashboard/widget-panel.hbs
Normal file
@@ -0,0 +1,32 @@
|
||||
<Overlay @isOpen={{@isOpen}} @onLoad={{this.setOverlayContext}} @position="right" @noBackdrop={{true}} @fullHeight={{true}} @width={{or this.width @width "400px"}}>
|
||||
<Overlay::Header @title={{"Select widgets"}} @hideStatusDot={{true}} @titleWrapperClass="leading-5">
|
||||
<div class="flex flex-1 justify-end">
|
||||
<Button @type="default" @icon="times" @helpText={{"Close and Save"}} @onClick={{this.onPressClose}} />
|
||||
</div>
|
||||
</Overlay::Header>
|
||||
|
||||
<Overlay::Body @wrapperClass="new-service-rate-overlay-body px-4 space-y-4 pt-4" @increaseInnerBodyHeightBy={{1000}}>
|
||||
<div class="grid grid-cols-1 gap-4 text-xs dark:text-gray-100">
|
||||
{{#each this.availableWidgets as |widget|}}
|
||||
<div
|
||||
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"
|
||||
{{on "click" (fn this.addWidgetToDashboard widget)}}
|
||||
>
|
||||
<div class="flex flex-row items-center leading-6 mb-2">
|
||||
<div class="w-8 flex items-center justify-start">
|
||||
<FaIcon @icon={{widget.icon}} class="text-2xl text-gray-600 dark:text-gray-300" />
|
||||
</div>
|
||||
<p class="text-sm truncate font-semibold dark:text-gray-100 text-gray-800">
|
||||
{{widget.name}}
|
||||
widget
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs dark:text-gray-100 text-gray-800">{{widget.description}}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
</Overlay::Body>
|
||||
</Overlay>
|
||||
56
console/app/components/dashboard/widget-panel.js
Normal file
56
console/app/components/dashboard/widget-panel.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
export default class DashboardWidgetPanelComponent extends Component {
|
||||
@service universe;
|
||||
@tracked availableWidgets = [];
|
||||
@tracked dashboard;
|
||||
@tracked isOpen = true;
|
||||
@service notifications;
|
||||
|
||||
/**
|
||||
* Constructs the component and applies initial state.
|
||||
*/
|
||||
constructor(owner, { dashboard }) {
|
||||
super(...arguments);
|
||||
|
||||
this.availableWidgets = this.universe.getDashboardWidgets();
|
||||
this.dashboard = dashboard;
|
||||
|
||||
console.log(this.availableWidgets, this.dashboard);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the overlay context.
|
||||
*
|
||||
* @action
|
||||
* @param {OverlayContextObject} overlayContext
|
||||
*/
|
||||
@action setOverlayContext(overlayContext) {
|
||||
this.context = overlayContext;
|
||||
|
||||
if (typeof this.args.onLoad === 'function') {
|
||||
this.args.onLoad(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
@action addWidgetToDashboard(widget) {
|
||||
console.log('Adding widget to dashboard: ', widget);
|
||||
this.args.dashboard.addWidget(widget).catch((error) => {
|
||||
this.notifications.serverError(error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles cancel button press.
|
||||
*
|
||||
* @action
|
||||
*/
|
||||
@action onPressClose() {
|
||||
this.isOpen = false;
|
||||
console.log(this.args);
|
||||
this.args.onClose();
|
||||
}
|
||||
}
|
||||
5
console/app/components/modals/create-dashboard.hbs
Normal file
5
console/app/components/modals/create-dashboard.hbs
Normal file
@@ -0,0 +1,5 @@
|
||||
<Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
|
||||
<div class="modal-body-container">
|
||||
<InputGroup @name="Dashboard name" @value={{@options.name}} @helpText="Enter the name of your dashboard" />
|
||||
</div>
|
||||
</Modal::Default>
|
||||
7
console/app/helpers/spread-widget-options.js
Normal file
7
console/app/helpers/spread-widget-options.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { helper } from '@ember/component/helper';
|
||||
|
||||
export default helper(function spreadWidgetOptions([params]) {
|
||||
const { id, options } = params;
|
||||
const gridOptions = { id, ...options };
|
||||
return gridOptions;
|
||||
});
|
||||
36
console/app/instance-initializers/initialize-widgets.js
Normal file
36
console/app/instance-initializers/initialize-widgets.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { faGithub } from '@fortawesome/free-brands-svg-icons';
|
||||
|
||||
export function initialize(application) {
|
||||
const universe = application.lookup('service:universe');
|
||||
const defaultWidgets = [
|
||||
{
|
||||
did: 'fleetbase-blog',
|
||||
name: 'Fleetbase Blog',
|
||||
description: 'Lists latest news and events from the Fleetbase official team.',
|
||||
icon: 'newspaper',
|
||||
component: 'fleetbase-blog',
|
||||
grid_options: { w: 8, h: 9, minW: 8, minH: 9 },
|
||||
options: {
|
||||
title: 'Fleetbase Blog',
|
||||
},
|
||||
},
|
||||
{
|
||||
did: 'github-card',
|
||||
name: 'Github Card',
|
||||
description: 'Displays current Github stats from the official Fleetbase repo.',
|
||||
icon: faGithub,
|
||||
component: 'github-card',
|
||||
grid_options: { w: 4, h: 8, minW: 4, minH: 8 },
|
||||
options: {
|
||||
title: 'Github Card',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
universe.registerDefaultDashboardWidgets(defaultWidgets);
|
||||
universe.registerDashboardWidgets(defaultWidgets);
|
||||
}
|
||||
|
||||
export default {
|
||||
initialize,
|
||||
};
|
||||
54
console/app/models/dashboard-widget.js
Normal file
54
console/app/models/dashboard-widget.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import Model, { attr, belongsTo } from '@ember-data/model';
|
||||
import { computed } from '@ember/object';
|
||||
import { format, formatDistanceToNow } from 'date-fns';
|
||||
import { getOwner } from '@ember/application';
|
||||
|
||||
export default class DashboardWidgetModel extends Model {
|
||||
/** @ids */
|
||||
@attr('string') uuid;
|
||||
@attr('string') dashboard_uuid;
|
||||
|
||||
/** @relationships */
|
||||
@belongsTo('dashboard') dashboard;
|
||||
|
||||
/** @attributes */
|
||||
@attr('string') name;
|
||||
@attr('string') component;
|
||||
@attr('object') grid_options;
|
||||
@attr('object') options;
|
||||
|
||||
/** @dates */
|
||||
@attr('date') created_at;
|
||||
@attr('date') updated_at;
|
||||
|
||||
/** @computed */
|
||||
@computed('updated_at') get updatedAgo() {
|
||||
return formatDistanceToNow(this.updated_at);
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAt() {
|
||||
return format(this.updated_at, 'PPP p');
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAtShort() {
|
||||
return format(this.updated_at, 'PP');
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAgo() {
|
||||
return formatDistanceToNow(this.created_at);
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAt() {
|
||||
return format(this.created_at, 'PPP p');
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAtShort() {
|
||||
return format(this.created_at, 'PP');
|
||||
}
|
||||
|
||||
updatePosition() {
|
||||
this.save().then((response) => {
|
||||
console.log('Widget updated successfully.', response);
|
||||
});
|
||||
}
|
||||
}
|
||||
89
console/app/models/dashboard.js
Normal file
89
console/app/models/dashboard.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import Model, { attr, hasMany } from '@ember-data/model';
|
||||
import { computed } from '@ember/object';
|
||||
import { format, formatDistanceToNow } from 'date-fns';
|
||||
import { getOwner } from '@ember/application';
|
||||
|
||||
export default class DashboardModel extends Model {
|
||||
/** @ids */
|
||||
@attr('string') owner_uuid;
|
||||
|
||||
/** @relationships */
|
||||
@hasMany('dashboard-widget', { async: false }) widgets;
|
||||
|
||||
/** @attributes */
|
||||
@attr('string') name;
|
||||
@attr('boolean') is_default;
|
||||
|
||||
/** @dates */
|
||||
@attr('date') created_at;
|
||||
@attr('date') updated_at;
|
||||
|
||||
/** @computed */
|
||||
@computed('updated_at') get updatedAgo() {
|
||||
return formatDistanceToNow(this.updated_at);
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAt() {
|
||||
return format(this.updated_at, 'PPP p');
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAtShort() {
|
||||
return format(this.updated_at, 'PP');
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAgo() {
|
||||
return formatDistanceToNow(this.created_at);
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAt() {
|
||||
return format(this.created_at, 'PPP p');
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAtShort() {
|
||||
return format(this.created_at, 'PP');
|
||||
}
|
||||
|
||||
/** @methods */
|
||||
addWidget(widget) {
|
||||
const owner = getOwner(this);
|
||||
const store = owner.lookup('service:store');
|
||||
|
||||
const widgetRecord = store.createRecord('dashboard-widget', widget);
|
||||
|
||||
widgetRecord.dashboard = this;
|
||||
console.log(widgetRecord);
|
||||
return new Promise((resolve, reject) => {
|
||||
widgetRecord
|
||||
.save()
|
||||
.then((widgetRecord) => {
|
||||
this.widgets.pushObject(widgetRecord);
|
||||
resolve(widgetRecord);
|
||||
})
|
||||
.catch((error) => {
|
||||
store.unloadRecord(widgetRecord);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
removeWidget(widget) {
|
||||
const owner = getOwner(this);
|
||||
const store = owner.lookup('service:store');
|
||||
|
||||
const widgetRecord = store.peekRecord('dashboard-widget', widget);
|
||||
|
||||
if (widgetRecord) {
|
||||
return new Promise((resolve, reject) => {
|
||||
widgetRecord
|
||||
.destroyRecord()
|
||||
.then(() => {
|
||||
this.widgets.removeObject(widgetRecord);
|
||||
resolve();
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
3
console/app/serializers/dashboard-widget.js
Normal file
3
console/app/serializers/dashboard-widget.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import ApplicationSerializer from '@fleetbase/ember-core/serializers/application';
|
||||
|
||||
export default class DashboardWidgetSerializer extends ApplicationSerializer {}
|
||||
18
console/app/serializers/dashboard.js
Normal file
18
console/app/serializers/dashboard.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import ApplicationSerializer from '@fleetbase/ember-core/serializers/application';
|
||||
import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest';
|
||||
|
||||
export default class DashboardSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) {
|
||||
attrs = {
|
||||
widgets: { embedded: 'always' },
|
||||
};
|
||||
|
||||
serializeHasMany(snapshot, json, relationship) {
|
||||
let key = relationship.key;
|
||||
|
||||
if (key === 'widgets') {
|
||||
return;
|
||||
}
|
||||
|
||||
return super.serializeHasMany(...arguments);
|
||||
}
|
||||
}
|
||||
126
console/app/services/dashboard.js
Normal file
126
console/app/services/dashboard.js
Normal file
@@ -0,0 +1,126 @@
|
||||
// app/services/dashboard.js
|
||||
import Service from '@ember/service';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { task } from 'ember-concurrency-decorators';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
export default class DashboardService extends Service {
|
||||
@service store;
|
||||
@service fetch;
|
||||
@service notifications;
|
||||
@service universe;
|
||||
|
||||
@tracked dashboards = [];
|
||||
@tracked currentDashboard;
|
||||
@tracked isEditingDashboard = false;
|
||||
@tracked isAddingWidget = false;
|
||||
|
||||
@task *loadDashboards() {
|
||||
try {
|
||||
const dashboards = yield this.store.findAll('dashboard');
|
||||
this.dashboards = dashboards.toArray();
|
||||
|
||||
if (dashboards.some((dashboard) => dashboard.owner_uuid !== 'system')) {
|
||||
this.dashboards.unshiftObject(this._createDefaultDashboard());
|
||||
}
|
||||
// Set the current dashboard
|
||||
this.currentDashboard = this.dashboards.find((dashboard) => dashboard.is_default) || this.dashboards[0];
|
||||
if (this.currentDashboard?.widgets?.length === 0) {
|
||||
this.onAddingWidget(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading dashboards:', error);
|
||||
}
|
||||
}
|
||||
|
||||
@task *selectDashboard(dashboard) {
|
||||
try {
|
||||
if (dashboard.owner_uuid === 'system') {
|
||||
this.currentDashboard = dashboard;
|
||||
return;
|
||||
}
|
||||
const response = yield this.fetch.post('dashboards/switch', { dashboard_uuid: dashboard.id });
|
||||
this.store.pushPayload(response);
|
||||
const selectedDashboardId = response.uuid;
|
||||
this.currentDashboard = this.store.peekRecord('dashboard', selectedDashboardId);
|
||||
|
||||
if (this.currentDashboard?.widgets?.length === 0) {
|
||||
this.onChangeEdit(true);
|
||||
}
|
||||
} catch (error) {
|
||||
this.notifications.error(`Error switching dashboard: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
@task *createDashboard(name, options = {}) {
|
||||
try {
|
||||
const newDashboard = this.store.createRecord('dashboard', { name, is_default: true });
|
||||
const response = yield newDashboard.save();
|
||||
|
||||
if (typeof options.successNotification === 'function') {
|
||||
this.notifications.success(options.successNotification(response));
|
||||
} else {
|
||||
this.notifications.success(options.successNotification || `${response.name} created.`);
|
||||
}
|
||||
|
||||
this.selectDashboard.perform(response);
|
||||
this.isEditingDashboard = true;
|
||||
} catch (error) {
|
||||
this.notifications.serverError(error);
|
||||
}
|
||||
}
|
||||
|
||||
@task *deleteDashboard(dashboard, options = {}) {
|
||||
try {
|
||||
yield dashboard.destroyRecord();
|
||||
this.notifications.success(options.successNotification || `${dashboard.name} has been deleted.`);
|
||||
this.loadDashboards.perform();
|
||||
} catch (error) {
|
||||
this.notifications.serverError(error);
|
||||
if (options.onError) {
|
||||
options.onError(error, dashboard);
|
||||
}
|
||||
} finally {
|
||||
if (options.callback) {
|
||||
options.callback(this.currentDashboard);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@task *setCurrentDashboard(dashboard) {
|
||||
try {
|
||||
const response = yield this.fetch.post('dashboards/switch', { dashboard_uuid: dashboard.id });
|
||||
this.currentDashboard = response;
|
||||
} catch (error) {
|
||||
this.notifications.error(`Error setting current dashboard: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
@action onChangeEdit(state = true) {
|
||||
this.isEditingDashboard = state;
|
||||
}
|
||||
|
||||
@action onAddingWidget(state = true) {
|
||||
this.isAddingWidget = state;
|
||||
}
|
||||
|
||||
_createDefaultDashboard() {
|
||||
const defaultDashboard = this.store.createRecord('dashboard', {
|
||||
name: 'Default Dashboard',
|
||||
is_default: false,
|
||||
owner_uuid: 'system',
|
||||
widgets: this._createDefaultDashboardWidgets(),
|
||||
});
|
||||
|
||||
return defaultDashboard;
|
||||
}
|
||||
|
||||
_createDefaultDashboardWidgets() {
|
||||
const widgets = this.universe.getDefaultDashboardWidgets().map((defaultWidget) => {
|
||||
return this.store.createRecord('dashboard-widget', defaultWidget);
|
||||
});
|
||||
|
||||
return widgets;
|
||||
}
|
||||
}
|
||||
@@ -18,4 +18,5 @@
|
||||
{{!-- Add Locale Selector to Header --}}
|
||||
<EmberWormhole @to="view-header-actions">
|
||||
<LocaleSelector class="mr-0.5" />
|
||||
</EmberWormhole>
|
||||
</EmberWormhole>
|
||||
<div id="console-wormhole" />
|
||||
@@ -1,11 +1,7 @@
|
||||
{{page-title "Dashboard"}}
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full pt-6">
|
||||
<div class="console-home-container mx-auto h-screen space-y-4" {{increase-height-by 300}}>
|
||||
<TwoFaEnforcementAlert />
|
||||
<div class="grid grid-cols-1 md:grid-cols-12 gap-4">
|
||||
<GithubCard class="lg:col-span-4" />
|
||||
<FleetbaseBlog class="lg:col-span-8" />
|
||||
</div>
|
||||
<Dashboard @sidebar={{this.sidebarContext}} />
|
||||
</div>
|
||||
</Layout::Section::Body>
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<TwoFaEnforcementAlert />
|
||||
<Dashboard @sidebar={{this.sidebarContext}} />
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
<div id="console-home-wormhole" />
|
||||
@@ -21,6 +21,7 @@
|
||||
"lint:hbs:fix": "ember-template-lint . --fix",
|
||||
"lint:js": "eslint . --cache",
|
||||
"lint:js:fix": "eslint . --fix",
|
||||
"postinstall": "patch-package",
|
||||
"lint:intl": "fleetbase-intl-lint",
|
||||
"start": "pnpm run prebuild && ember serve",
|
||||
"test": "concurrently \"npm:lint\" \"npm:test:*\" --names \"lint,test:\"",
|
||||
@@ -42,6 +43,7 @@
|
||||
"ember-composable-helpers": "^5.0.0",
|
||||
"ember-concurrency": "^3.0.0",
|
||||
"ember-concurrency-decorators": "^2.0.3",
|
||||
"ember-gridstack": "^4.0.0",
|
||||
"ember-intl": "6.3.2",
|
||||
"ember-math-helpers": "^2.18.2",
|
||||
"ember-power-select": "^6.0.1",
|
||||
@@ -49,6 +51,8 @@
|
||||
"ember-radio-button": "3.0.0-beta.1",
|
||||
"ember-tag-input": "^3.1.0",
|
||||
"fleetbase-extensions-indexer": "^0.0.4",
|
||||
"gridstack": "^7.2.2",
|
||||
"patch-package": "^8.0.0",
|
||||
"postcss-at-rules-variables": "^0.3.0",
|
||||
"postcss-custom-properties": "^12.1.9",
|
||||
"postcss-nth-list": "^1.0.2"
|
||||
|
||||
11
console/patches/ember-gridstack+4.0.0.patch
Normal file
11
console/patches/ember-gridstack+4.0.0.patch
Normal file
@@ -0,0 +1,11 @@
|
||||
diff --git a/node_modules/ember-gridstack/addon/components/grid-stack.js b/node_modules/ember-gridstack/addon/components/grid-stack.js
|
||||
index fa51392..fdabb2a 100644
|
||||
--- a/node_modules/ember-gridstack/addon/components/grid-stack.js
|
||||
+++ b/node_modules/ember-gridstack/addon/components/grid-stack.js
|
||||
@@ -133,5 +133,6 @@ export default class GridStackComponent extends Component {
|
||||
removeWidget(element, removeDOM = false, triggerEvent = true) {
|
||||
triggerEvent = triggerEvent && !this.isDestroying && !this.isDestroyed;
|
||||
this.gridStack?.removeWidget(element, removeDOM, triggerEvent);
|
||||
+ this.gridStack?.compact();
|
||||
}
|
||||
}
|
||||
86
console/pnpm-lock.yaml
generated
86
console/pnpm-lock.yaml
generated
@@ -51,6 +51,9 @@ dependencies:
|
||||
ember-concurrency-decorators:
|
||||
specifier: ^2.0.3
|
||||
version: 2.0.3(@babel/core@7.23.2)
|
||||
ember-gridstack:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0(@babel/core@7.23.2)(ember-source@5.4.0)(webpack@5.89.0)
|
||||
ember-intl:
|
||||
specifier: 6.3.2
|
||||
version: 6.3.2(@babel/core@7.23.2)(webpack@5.89.0)
|
||||
@@ -72,6 +75,12 @@ dependencies:
|
||||
fleetbase-extensions-indexer:
|
||||
specifier: ^0.0.4
|
||||
version: 0.0.4
|
||||
gridstack:
|
||||
specifier: ^7.2.2
|
||||
version: 7.2.2
|
||||
patch-package:
|
||||
specifier: ^8.0.0
|
||||
version: 8.0.0
|
||||
postcss-at-rules-variables:
|
||||
specifier: ^0.3.0
|
||||
version: 0.3.0(postcss@8.4.21)
|
||||
@@ -4102,6 +4111,10 @@ packages:
|
||||
/@xtuc/long@4.2.2:
|
||||
resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==}
|
||||
|
||||
/@yarnpkg/lockfile@1.1.0:
|
||||
resolution: {integrity: sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==}
|
||||
dev: false
|
||||
|
||||
/@zestia/ember-dragula@12.1.0(@babel/core@7.23.2)(ember-source@5.4.0)(webpack@5.89.0):
|
||||
resolution: {integrity: sha512-iDc0qgdHobvMuoXB0tij+cVR8xmaEdBml97XwUkykbu7st7W0E6waWo65Qei+d8wHlCoFxchd9/7HOXHT0+73Q==}
|
||||
engines: {node: 16.* || >= 18}
|
||||
@@ -6149,7 +6162,6 @@ packages:
|
||||
/ci-info@3.9.0:
|
||||
resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/cipher-base@1.0.4:
|
||||
resolution: {integrity: sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==}
|
||||
@@ -8519,6 +8531,26 @@ packages:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/ember-gridstack@4.0.0(@babel/core@7.23.2)(ember-source@5.4.0)(webpack@5.89.0):
|
||||
resolution: {integrity: sha512-qcl3E+k7wWO7D/zqTzU4jtu6ruVYFT/SbBoqaOBZR017aBDOo3zcavU80z6w8zNYqUG6gVWSXFKNTtbE/D2wlg==}
|
||||
engines: {node: 14.* || 16.* || >= 18}
|
||||
peerDependencies:
|
||||
ember-source: ^4.0.0
|
||||
dependencies:
|
||||
'@ember/render-modifiers': 2.1.0(@babel/core@7.23.2)(ember-source@5.4.0)
|
||||
ember-auto-import: 2.7.2(webpack@5.89.0)
|
||||
ember-cli-babel: 7.26.11
|
||||
ember-cli-htmlbars: 6.3.0
|
||||
ember-modifier: 4.1.0(ember-source@5.4.0)
|
||||
ember-source: 5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2)(rsvp@4.8.5)(webpack@5.89.0)
|
||||
gridstack: 7.2.2
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- '@glint/template'
|
||||
- supports-color
|
||||
- webpack
|
||||
dev: false
|
||||
|
||||
/ember-in-element-polyfill@1.0.1:
|
||||
resolution: {integrity: sha512-eHs+7D7PuQr8a1DPqsJTsEyo3FZ1XuH6WEZaEBPDa9s0xLlwByCNKl8hi1EbXOgvgEZNHHi9Rh0vjxyfakrlgg==}
|
||||
engines: {node: 10.* || >= 12}
|
||||
@@ -10691,6 +10723,10 @@ packages:
|
||||
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
|
||||
dev: true
|
||||
|
||||
/gridstack@7.2.2:
|
||||
resolution: {integrity: sha512-9swEjbisKhtZlbmNiTCxOarp/9NWit5mLg6Z73sUhd4LKur5ZptMH16CUJu7HjMHxgI86FbQI5ZfMM/2TuMqdw==}
|
||||
dev: false
|
||||
|
||||
/growly@1.3.0:
|
||||
resolution: {integrity: sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==}
|
||||
dev: true
|
||||
@@ -11243,7 +11279,6 @@ packages:
|
||||
resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
|
||||
engines: {node: '>=8'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/is-extendable@0.1.1:
|
||||
resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==}
|
||||
@@ -11450,7 +11485,6 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
is-docker: 2.2.1
|
||||
dev: true
|
||||
|
||||
/isarray@0.0.1:
|
||||
resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==}
|
||||
@@ -11639,6 +11673,12 @@ packages:
|
||||
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
/klaw-sync@6.0.0:
|
||||
resolution: {integrity: sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==}
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
dev: false
|
||||
|
||||
/klaw@1.3.1:
|
||||
resolution: {integrity: sha512-TED5xi9gGQjGpNnvRWknrwAB1eL5GciPfVFOt3Vk1OJCVDQbzuSfrF3hkUQKlsgKrG1F+0t5W0m+Fje1jIt8rw==}
|
||||
optionalDependencies:
|
||||
@@ -12761,6 +12801,14 @@ packages:
|
||||
dependencies:
|
||||
mimic-fn: 2.1.0
|
||||
|
||||
/open@7.4.2:
|
||||
resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
is-docker: 2.2.1
|
||||
is-wsl: 2.2.0
|
||||
dev: false
|
||||
|
||||
/optionator@0.9.3:
|
||||
resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -12996,6 +13044,28 @@ packages:
|
||||
resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
/patch-package@8.0.0:
|
||||
resolution: {integrity: sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==}
|
||||
engines: {node: '>=14', npm: '>5'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
'@yarnpkg/lockfile': 1.1.0
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.9.0
|
||||
cross-spawn: 7.0.3
|
||||
find-yarn-workspace-root: 2.0.0
|
||||
fs-extra: 9.1.0
|
||||
json-stable-stringify: 1.1.1
|
||||
klaw-sync: 6.0.0
|
||||
minimist: 1.2.8
|
||||
open: 7.4.2
|
||||
rimraf: 2.7.1
|
||||
semver: 7.5.4
|
||||
slash: 2.0.0
|
||||
tmp: 0.0.33
|
||||
yaml: 2.3.4
|
||||
dev: false
|
||||
|
||||
/path-browserify@0.0.1:
|
||||
resolution: {integrity: sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==}
|
||||
dev: false
|
||||
@@ -14985,6 +15055,11 @@ packages:
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/slash@2.0.0:
|
||||
resolution: {integrity: sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==}
|
||||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/slash@3.0.0:
|
||||
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -16711,6 +16786,11 @@ packages:
|
||||
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
/yaml@2.3.4:
|
||||
resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==}
|
||||
engines: {node: '>= 14'}
|
||||
dev: false
|
||||
|
||||
/yargs-parser@20.2.9:
|
||||
resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from '@fleetbase/console/tests/helpers';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Component | dashboard/widget-panel', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
// Set any properties with this.set('myProperty', 'value');
|
||||
// Handle any actions with this.set('myAction', function(val) { ... });
|
||||
|
||||
await render(hbs`<Dashboard::WidgetPanel />`);
|
||||
|
||||
assert.dom().hasText('');
|
||||
|
||||
// Template block usage:
|
||||
await render(hbs`
|
||||
<Dashboard::WidgetPanel>
|
||||
template block text
|
||||
</Dashboard::WidgetPanel>
|
||||
`);
|
||||
|
||||
assert.dom().hasText('template block text');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from '@fleetbase/console/tests/helpers';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Helper | spread-widget-options', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it renders', async function (assert) {
|
||||
this.set('inputValue', '1234');
|
||||
|
||||
await render(hbs`{{spread-widget-options this.inputValue}}`);
|
||||
|
||||
assert.dom().hasText('1234');
|
||||
});
|
||||
});
|
||||
14
console/tests/unit/models/dashboard-test.js
Normal file
14
console/tests/unit/models/dashboard-test.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { module, test } from 'qunit';
|
||||
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Model | dashboard', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let model = store.createRecord('dashboard', {});
|
||||
assert.ok(model);
|
||||
});
|
||||
});
|
||||
14
console/tests/unit/models/dashboard-widget-test.js
Normal file
14
console/tests/unit/models/dashboard-widget-test.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { module, test } from 'qunit';
|
||||
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Model | dashboard widget', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let model = store.createRecord('dashboard-widget', {});
|
||||
assert.ok(model);
|
||||
});
|
||||
});
|
||||
24
console/tests/unit/serializers/dashboard-test.js
Normal file
24
console/tests/unit/serializers/dashboard-test.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { module, test } from 'qunit';
|
||||
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Serializer | dashboard', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let serializer = store.serializerFor('dashboard');
|
||||
|
||||
assert.ok(serializer);
|
||||
});
|
||||
|
||||
test('it serializes records', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let record = store.createRecord('dashboard', {});
|
||||
|
||||
let serializedRecord = record.serialize();
|
||||
|
||||
assert.ok(serializedRecord);
|
||||
});
|
||||
});
|
||||
24
console/tests/unit/serializers/dashboard-widget-test.js
Normal file
24
console/tests/unit/serializers/dashboard-widget-test.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { module, test } from 'qunit';
|
||||
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Serializer | dashboard widget', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let serializer = store.serializerFor('dashboard-widget');
|
||||
|
||||
assert.ok(serializer);
|
||||
});
|
||||
|
||||
test('it serializes records', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let record = store.createRecord('dashboard-widget', {});
|
||||
|
||||
let serializedRecord = record.serialize();
|
||||
|
||||
assert.ok(serializedRecord);
|
||||
});
|
||||
});
|
||||
12
console/tests/unit/services/dashboard-test.js
Normal file
12
console/tests/unit/services/dashboard-test.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Service | dashboard', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let service = this.owner.lookup('service:dashboard');
|
||||
assert.ok(service);
|
||||
});
|
||||
});
|
||||
@@ -23,16 +23,16 @@ services:
|
||||
SOCKETCLUSTER_WORKERS: 10
|
||||
SOCKETCLUSTER_BROKERS: 10
|
||||
|
||||
console:
|
||||
ports:
|
||||
- "4200:4200"
|
||||
volumes:
|
||||
- ./console:/app/console
|
||||
build:
|
||||
context: .
|
||||
dockerfile: console/Dockerfile
|
||||
args:
|
||||
ENVIRONMENT: development
|
||||
# console:
|
||||
# ports:
|
||||
# - "4200:4200"
|
||||
# volumes:
|
||||
# - ./console:/app/console
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: console/Dockerfile
|
||||
# args:
|
||||
# ENVIRONMENT: development
|
||||
|
||||
application:
|
||||
build:
|
||||
|
||||
Reference in New Issue
Block a user