mirror of
https://github.com/fleetbase/fleetbase.git
synced 2025-12-19 22:27:22 +00:00
v0.7.11 ~ Maintenance, Reports, Telematics Upgrade/Features
This commit is contained in:
@@ -5,6 +5,7 @@ import config from '@fleetbase/console/config/environment';
|
||||
import loadExtensions from '@fleetbase/ember-core/utils/load-extensions';
|
||||
import mapEngines from '@fleetbase/ember-core/utils/map-engines';
|
||||
import loadRuntimeConfig from '@fleetbase/console/utils/runtime-config';
|
||||
import applyRouterFix from './utils/router-refresh-patch';
|
||||
|
||||
export default class App extends Application {
|
||||
modulePrefix = config.modulePrefix;
|
||||
@@ -14,6 +15,7 @@ export default class App extends Application {
|
||||
engines = {};
|
||||
|
||||
async ready() {
|
||||
applyRouterFix(this);
|
||||
const extensions = await loadExtensions();
|
||||
|
||||
this.extensions = extensions;
|
||||
|
||||
304
console/app/models/alert.js
Normal file
304
console/app/models/alert.js
Normal file
@@ -0,0 +1,304 @@
|
||||
import Model, { attr, belongsTo } from '@ember-data/model';
|
||||
import { computed } from '@ember/object';
|
||||
import { format, formatDistanceToNow, differenceInMinutes } from 'date-fns';
|
||||
|
||||
export default class AlertModel extends Model {
|
||||
/** @attributes */
|
||||
@attr('string') type;
|
||||
@attr('string') severity;
|
||||
@attr('string') status;
|
||||
@attr('string') subject_type;
|
||||
@attr('string') subject_uuid;
|
||||
@attr('string') message;
|
||||
|
||||
/** @json attributes */
|
||||
@attr() rule;
|
||||
@attr() context;
|
||||
@attr() meta;
|
||||
|
||||
/** @dates */
|
||||
@attr('date') triggered_at;
|
||||
@attr('date') acknowledged_at;
|
||||
@attr('date') resolved_at;
|
||||
@attr('date') created_at;
|
||||
@attr('date') updated_at;
|
||||
@attr('date') deleted_at;
|
||||
|
||||
/** @relationships */
|
||||
@belongsTo('company') company;
|
||||
@belongsTo('user', { inverse: null }) acknowledgedBy;
|
||||
@belongsTo('user', { inverse: null }) resolvedBy;
|
||||
|
||||
/** @computed - Date formatting */
|
||||
@computed('triggered_at') get triggeredAgo() {
|
||||
if (!this.triggered_at) return 'Unknown';
|
||||
return formatDistanceToNow(this.triggered_at) + ' ago';
|
||||
}
|
||||
|
||||
@computed('triggered_at') get triggeredAt() {
|
||||
if (!this.triggered_at) return 'Unknown';
|
||||
return format(this.triggered_at, 'PPP p');
|
||||
}
|
||||
|
||||
@computed('acknowledged_at') get acknowledgedAgo() {
|
||||
if (!this.acknowledged_at) return null;
|
||||
return formatDistanceToNow(this.acknowledged_at) + ' ago';
|
||||
}
|
||||
|
||||
@computed('acknowledged_at') get acknowledgedAt() {
|
||||
if (!this.acknowledged_at) return 'Not acknowledged';
|
||||
return format(this.acknowledged_at, 'PPP p');
|
||||
}
|
||||
|
||||
@computed('resolved_at') get resolvedAgo() {
|
||||
if (!this.resolved_at) return null;
|
||||
return formatDistanceToNow(this.resolved_at) + ' ago';
|
||||
}
|
||||
|
||||
@computed('resolved_at') get resolvedAt() {
|
||||
if (!this.resolved_at) return 'Not resolved';
|
||||
return format(this.resolved_at, 'PPP p');
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAgo() {
|
||||
return formatDistanceToNow(this.updated_at) + ' ago';
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAt() {
|
||||
return format(this.updated_at, 'PPP p');
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAgo() {
|
||||
return formatDistanceToNow(this.created_at) + ' ago';
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAt() {
|
||||
return format(this.created_at, 'PPP p');
|
||||
}
|
||||
|
||||
/** @computed - Status checks */
|
||||
@computed('acknowledged_at') get isAcknowledged() {
|
||||
return !!this.acknowledged_at;
|
||||
}
|
||||
|
||||
@computed('resolved_at') get isResolved() {
|
||||
return !!this.resolved_at;
|
||||
}
|
||||
|
||||
@computed('isAcknowledged', 'isResolved') get isPending() {
|
||||
return !this.isAcknowledged && !this.isResolved;
|
||||
}
|
||||
|
||||
@computed('isAcknowledged', 'isResolved') get isActive() {
|
||||
return this.isAcknowledged && !this.isResolved;
|
||||
}
|
||||
|
||||
/** @computed - Duration calculations */
|
||||
@computed('triggered_at', 'acknowledged_at') get acknowledgmentDurationMinutes() {
|
||||
if (!this.triggered_at || !this.acknowledged_at) return null;
|
||||
return differenceInMinutes(new Date(this.acknowledged_at), new Date(this.triggered_at));
|
||||
}
|
||||
|
||||
@computed('triggered_at', 'resolved_at') get resolutionDurationMinutes() {
|
||||
if (!this.triggered_at || !this.resolved_at) return null;
|
||||
return differenceInMinutes(new Date(this.resolved_at), new Date(this.triggered_at));
|
||||
}
|
||||
|
||||
@computed('triggered_at') get ageMinutes() {
|
||||
if (!this.triggered_at) return 0;
|
||||
return differenceInMinutes(new Date(), new Date(this.triggered_at));
|
||||
}
|
||||
|
||||
@computed('acknowledgmentDurationMinutes') get acknowledgmentDurationFormatted() {
|
||||
if (!this.acknowledgmentDurationMinutes) return null;
|
||||
|
||||
const minutes = this.acknowledgmentDurationMinutes;
|
||||
if (minutes < 60) return `${minutes}m`;
|
||||
if (minutes < 1440) return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
|
||||
return `${Math.floor(minutes / 1440)}d ${Math.floor((minutes % 1440) / 60)}h`;
|
||||
}
|
||||
|
||||
@computed('resolutionDurationMinutes') get resolutionDurationFormatted() {
|
||||
if (!this.resolutionDurationMinutes) return null;
|
||||
|
||||
const minutes = this.resolutionDurationMinutes;
|
||||
if (minutes < 60) return `${minutes}m`;
|
||||
if (minutes < 1440) return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
|
||||
return `${Math.floor(minutes / 1440)}d ${Math.floor((minutes % 1440) / 60)}h`;
|
||||
}
|
||||
|
||||
@computed('ageMinutes') get ageFormatted() {
|
||||
const minutes = this.ageMinutes;
|
||||
if (minutes < 60) return `${minutes}m`;
|
||||
if (minutes < 1440) return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
|
||||
return `${Math.floor(minutes / 1440)}d ${Math.floor((minutes % 1440) / 60)}h`;
|
||||
}
|
||||
|
||||
/** @computed - Severity styling */
|
||||
@computed('severity') get severityBadgeClass() {
|
||||
const severityClasses = {
|
||||
'critical': 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300',
|
||||
'high': 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300',
|
||||
'medium': 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300',
|
||||
'low': 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300',
|
||||
'info': 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'
|
||||
};
|
||||
return severityClasses[this.severity] || severityClasses['info'];
|
||||
}
|
||||
|
||||
@computed('severity') get severityIcon() {
|
||||
const severityIcons = {
|
||||
'critical': 'fas fa-exclamation-circle',
|
||||
'high': 'fas fa-exclamation-triangle',
|
||||
'medium': 'fas fa-exclamation',
|
||||
'low': 'fas fa-info-circle',
|
||||
'info': 'fas fa-info'
|
||||
};
|
||||
return severityIcons[this.severity] || severityIcons['info'];
|
||||
}
|
||||
|
||||
@computed('severity') get severityColor() {
|
||||
const severityColors = {
|
||||
'critical': 'text-red-600 dark:text-red-400',
|
||||
'high': 'text-orange-600 dark:text-orange-400',
|
||||
'medium': 'text-yellow-600 dark:text-yellow-400',
|
||||
'low': 'text-blue-600 dark:text-blue-400',
|
||||
'info': 'text-gray-600 dark:text-gray-400'
|
||||
};
|
||||
return severityColors[this.severity] || severityColors['info'];
|
||||
}
|
||||
|
||||
/** @computed - Status styling */
|
||||
@computed('status', 'isAcknowledged', 'isResolved') get statusBadgeClass() {
|
||||
if (this.isResolved) {
|
||||
return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300';
|
||||
}
|
||||
if (this.isAcknowledged) {
|
||||
return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300';
|
||||
}
|
||||
return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300';
|
||||
}
|
||||
|
||||
@computed('status', 'isAcknowledged', 'isResolved') get statusText() {
|
||||
if (this.isResolved) return 'Resolved';
|
||||
if (this.isAcknowledged) return 'Acknowledged';
|
||||
return 'Pending';
|
||||
}
|
||||
|
||||
@computed('status', 'isAcknowledged', 'isResolved') get statusIcon() {
|
||||
if (this.isResolved) return 'fas fa-check-circle';
|
||||
if (this.isAcknowledged) return 'fas fa-eye';
|
||||
return 'fas fa-bell';
|
||||
}
|
||||
|
||||
/** @computed - Type styling */
|
||||
@computed('type') get typeIcon() {
|
||||
const typeIcons = {
|
||||
'maintenance': 'fas fa-wrench',
|
||||
'temperature': 'fas fa-thermometer-half',
|
||||
'fuel': 'fas fa-gas-pump',
|
||||
'speed': 'fas fa-tachometer-alt',
|
||||
'location': 'fas fa-map-marker-alt',
|
||||
'system': 'fas fa-cog',
|
||||
'security': 'fas fa-shield-alt',
|
||||
'performance': 'fas fa-chart-line',
|
||||
'compliance': 'fas fa-clipboard-check'
|
||||
};
|
||||
return typeIcons[this.type] || 'fas fa-bell';
|
||||
}
|
||||
|
||||
@computed('type') get typeBadgeClass() {
|
||||
const typeClasses = {
|
||||
'maintenance': 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300',
|
||||
'temperature': 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300',
|
||||
'fuel': 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300',
|
||||
'speed': 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300',
|
||||
'location': 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300',
|
||||
'system': 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300',
|
||||
'security': 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300',
|
||||
'performance': 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-300',
|
||||
'compliance': 'bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-300'
|
||||
};
|
||||
return typeClasses[this.type] || 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300';
|
||||
}
|
||||
|
||||
/** @computed - Subject information */
|
||||
@computed('subject_type') get subjectTypeFormatted() {
|
||||
if (!this.subject_type) return 'Unknown';
|
||||
|
||||
// Convert from model class name to human readable
|
||||
const typeMap = {
|
||||
'vehicle': 'Vehicle',
|
||||
'driver': 'Driver',
|
||||
'order': 'Order',
|
||||
'device': 'Device',
|
||||
'asset': 'Asset',
|
||||
'maintenance': 'Maintenance',
|
||||
'fuel_report': 'Fuel Report'
|
||||
};
|
||||
|
||||
return typeMap[this.subject_type] || this.subject_type.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
||||
}
|
||||
|
||||
/** @computed - Priority and urgency */
|
||||
@computed('severity', 'ageMinutes') get urgencyLevel() {
|
||||
const severityWeight = {
|
||||
'critical': 4,
|
||||
'high': 3,
|
||||
'medium': 2,
|
||||
'low': 1,
|
||||
'info': 0
|
||||
};
|
||||
|
||||
const weight = severityWeight[this.severity] || 0;
|
||||
const ageHours = this.ageMinutes / 60;
|
||||
|
||||
// Calculate urgency based on severity and age
|
||||
if (weight >= 3 && ageHours > 1) return 'urgent';
|
||||
if (weight >= 2 && ageHours > 4) return 'urgent';
|
||||
if (weight >= 3) return 'high';
|
||||
if (weight >= 2) return 'medium';
|
||||
return 'low';
|
||||
}
|
||||
|
||||
@computed('urgencyLevel') get urgencyBadgeClass() {
|
||||
const urgencyClasses = {
|
||||
'urgent': 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300 animate-pulse',
|
||||
'high': 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300',
|
||||
'medium': 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300',
|
||||
'low': 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300'
|
||||
};
|
||||
return urgencyClasses[this.urgencyLevel] || urgencyClasses['low'];
|
||||
}
|
||||
|
||||
/** @computed - Context information */
|
||||
@computed('context') get hasContext() {
|
||||
return !!(this.context && Object.keys(this.context).length > 0);
|
||||
}
|
||||
|
||||
@computed('rule') get hasRule() {
|
||||
return !!(this.rule && Object.keys(this.rule).length > 0);
|
||||
}
|
||||
|
||||
@computed('context.location') get hasLocation() {
|
||||
return !!(this.context?.location);
|
||||
}
|
||||
|
||||
@computed('context.value', 'rule.threshold') get thresholdExceeded() {
|
||||
if (!this.context?.value || !this.rule?.threshold) return null;
|
||||
|
||||
const value = parseFloat(this.context.value);
|
||||
const threshold = parseFloat(this.rule.threshold);
|
||||
const operator = this.rule.operator || '>';
|
||||
|
||||
switch (operator) {
|
||||
case '>': return value > threshold;
|
||||
case '<': return value < threshold;
|
||||
case '>=': return value >= threshold;
|
||||
case '<=': return value <= threshold;
|
||||
case '==': return value === threshold;
|
||||
case '!=': return value !== threshold;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,14 @@ export default class CustomFieldModel extends Model {
|
||||
@attr('date') deleted_at;
|
||||
|
||||
/** @computed */
|
||||
@computed('type') get valueType() {
|
||||
if (this.type === 'file-upload') return 'file';
|
||||
if (this.type === 'date-time-input') return 'date';
|
||||
if (this.type === 'model-select') return 'model';
|
||||
|
||||
return 'text';
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAgo() {
|
||||
return formatDistanceToNow(this.created_at);
|
||||
}
|
||||
|
||||
510
console/app/models/report.js
Normal file
510
console/app/models/report.js
Normal file
@@ -0,0 +1,510 @@
|
||||
import Model, { attr, belongsTo, hasMany } from '@ember-data/model';
|
||||
import { computed } from '@ember/object';
|
||||
import { isArray } from '@ember/array';
|
||||
import { getOwner } from '@ember/application';
|
||||
import { isPresent, isEmpty } from '@ember/utils';
|
||||
|
||||
export default class ReportModel extends Model {
|
||||
/** @ids */
|
||||
@attr('string') public_id;
|
||||
@attr('string') company_uuid;
|
||||
@attr('string') created_by_uuid;
|
||||
@attr('string') category_uuid;
|
||||
@attr('string') subject_uuid;
|
||||
|
||||
/** @attributes */
|
||||
@attr('string') subject_type;
|
||||
@attr('string') title;
|
||||
@attr('string') description;
|
||||
@attr('date') period_start;
|
||||
@attr('date') period_end;
|
||||
@attr('date') last_executed_at;
|
||||
@attr('number') execution_time;
|
||||
@attr('number') row_count;
|
||||
@attr('boolean') is_scheduled;
|
||||
@attr('boolean') is_generated;
|
||||
@attr('string') status;
|
||||
@attr('string') type;
|
||||
@attr('raw') export_formats;
|
||||
@attr('raw') schedule_config;
|
||||
@attr('raw') data;
|
||||
@attr('raw') result_columns;
|
||||
@attr('raw') query_config;
|
||||
@attr('raw') tags;
|
||||
@attr('raw') options;
|
||||
@attr('raw') meta;
|
||||
@attr('string') status;
|
||||
|
||||
/** @relationships */
|
||||
// @belongsTo('company') company;
|
||||
// @belongsTo('user') createdBy;
|
||||
// @hasMany('report-execution') executions;
|
||||
// @hasMany('report-audit-log') auditLogs;
|
||||
|
||||
fillResult(result = {}) {
|
||||
this.setProperties({
|
||||
result_columns: result?.columns ?? [],
|
||||
data: result?.data ?? [],
|
||||
meta: result?.meta ?? {},
|
||||
row_count: result?.meta?.total_rows ?? 0,
|
||||
execution_time: result?.meta?.execution_time_ms ?? -1,
|
||||
last_executed_at: new Date(),
|
||||
is_generated: true,
|
||||
});
|
||||
}
|
||||
|
||||
/** @computed */
|
||||
@computed('query_config')
|
||||
get hasValidConfig() {
|
||||
return (
|
||||
isPresent(this.query_config) &&
|
||||
isPresent(this.query_config.table) &&
|
||||
isPresent(this.query_config.table.name) &&
|
||||
isArray(this.query_config.columns) &&
|
||||
this.query_config.columns.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
@computed('query_config.table.name')
|
||||
get tableName() {
|
||||
return this.query_config?.table?.name || '';
|
||||
}
|
||||
|
||||
@computed('query_config.table.label')
|
||||
get tableLabel() {
|
||||
return this.query_config?.table?.label || this.tableName;
|
||||
}
|
||||
|
||||
@computed('query_config.columns.[]')
|
||||
get selectedColumns() {
|
||||
return this.query_config?.columns || [];
|
||||
}
|
||||
|
||||
@computed('selectedColumns.[]', 'query_config.joins.[]')
|
||||
get totalSelectedColumns() {
|
||||
let count = this.selectedColumns.length;
|
||||
|
||||
// Add columns from joins
|
||||
if (isArray(this.query_config?.joins)) {
|
||||
this.query_config.joins.forEach((join) => {
|
||||
if (isArray(join.selectedColumns)) {
|
||||
count += join.selectedColumns.length;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
@computed('query_config.joins.[]')
|
||||
get hasJoins() {
|
||||
return isArray(this.query_config?.joins) && this.query_config.joins.length > 0;
|
||||
}
|
||||
|
||||
@computed('query_config.joins.[]')
|
||||
get joinedTables() {
|
||||
if (!this.hasJoins) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.query_config.joins.map((join) => ({
|
||||
table: join.table,
|
||||
label: join.label || join.table,
|
||||
type: join.type,
|
||||
columnsCount: join.selectedColumns?.length || 0,
|
||||
}));
|
||||
}
|
||||
|
||||
@computed('query_config.conditions.[]')
|
||||
get hasConditions() {
|
||||
return isArray(this.query_config?.conditions) && this.query_config.conditions.length > 0;
|
||||
}
|
||||
|
||||
@computed('query_config.conditions.[]')
|
||||
get conditionsCount() {
|
||||
if (!this.hasConditions) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this.countConditionsRecursively(this.query_config.conditions);
|
||||
}
|
||||
|
||||
@computed('query_config.groupBy.[]')
|
||||
get hasGrouping() {
|
||||
return isArray(this.query_config?.groupBy) && this.query_config.groupBy.length > 0;
|
||||
}
|
||||
|
||||
@computed('query_config.sortBy.[]')
|
||||
get hasSorting() {
|
||||
return isArray(this.query_config?.sortBy) && this.query_config.sortBy.length > 0;
|
||||
}
|
||||
|
||||
@computed('query_config.limit')
|
||||
get hasLimit() {
|
||||
return isPresent(this.query_config?.limit) && this.query_config.limit > 0;
|
||||
}
|
||||
|
||||
@computed('totalSelectedColumns', 'hasJoins', 'conditionsCount', 'hasGrouping')
|
||||
get complexity() {
|
||||
let score = 0;
|
||||
|
||||
score += this.totalSelectedColumns;
|
||||
score += this.hasJoins ? this.joinedTables.length * 3 : 0;
|
||||
score += this.conditionsCount * 2;
|
||||
score += this.hasGrouping ? 5 : 0;
|
||||
|
||||
if (score < 10) {
|
||||
return 'simple';
|
||||
} else if (score < 25) {
|
||||
return 'moderate';
|
||||
} else {
|
||||
return 'complex';
|
||||
}
|
||||
}
|
||||
|
||||
@computed('complexity', 'totalSelectedColumns', 'joinedTables.length')
|
||||
get estimatedPerformance() {
|
||||
if (this.complexity === 'simple' && this.totalSelectedColumns <= 10) {
|
||||
return 'fast';
|
||||
} else if (this.complexity === 'moderate' && this.joinedTables.length <= 2) {
|
||||
return 'moderate';
|
||||
} else {
|
||||
return 'slow';
|
||||
}
|
||||
}
|
||||
|
||||
@computed('last_executed_at')
|
||||
get lastExecutedDisplay() {
|
||||
if (!this.last_executed_at) {
|
||||
return 'Never executed';
|
||||
}
|
||||
|
||||
return this.last_executed_at.toLocaleDateString() + ' ' + this.last_executed_at.toLocaleTimeString();
|
||||
}
|
||||
|
||||
@computed('average_execution_time')
|
||||
get averageExecutionTimeDisplay() {
|
||||
if (!this.average_execution_time) {
|
||||
return 'N/A';
|
||||
}
|
||||
|
||||
if (this.average_execution_time < 1000) {
|
||||
return `${Math.round(this.average_execution_time)}ms`;
|
||||
} else {
|
||||
return `${(this.average_execution_time / 1000).toFixed(2)}s`;
|
||||
}
|
||||
}
|
||||
|
||||
@computed('execution_count')
|
||||
get executionCountDisplay() {
|
||||
return this.execution_count || 0;
|
||||
}
|
||||
|
||||
@computed('last_result_count')
|
||||
get lastResultCountDisplay() {
|
||||
if (this.last_result_count === null || this.last_result_count === undefined) {
|
||||
return 'N/A';
|
||||
}
|
||||
|
||||
return this.last_result_count.toLocaleString();
|
||||
}
|
||||
|
||||
@computed('export_formats.[]')
|
||||
get availableExportFormats() {
|
||||
return this.export_formats || ['csv', 'excel', 'json'];
|
||||
}
|
||||
|
||||
@computed('tags.[]')
|
||||
get tagsList() {
|
||||
return this.tags || [];
|
||||
}
|
||||
|
||||
@computed('shared_with.[]')
|
||||
get sharedWithList() {
|
||||
return this.shared_with || [];
|
||||
}
|
||||
|
||||
@computed('is_scheduled', 'schedule_frequency', 'next_scheduled_run')
|
||||
get scheduleInfo() {
|
||||
if (!this.is_scheduled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
frequency: this.schedule_frequency,
|
||||
nextRun: this.next_scheduled_run,
|
||||
timezone: this.schedule_timezone || 'UTC',
|
||||
};
|
||||
}
|
||||
|
||||
@computed('query_config.conditions.[]')
|
||||
get conditionsSummary() {
|
||||
if (!this.hasConditions) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.extractConditionsSummary(this.query_config.conditions);
|
||||
}
|
||||
|
||||
@computed('status')
|
||||
get statusDisplay() {
|
||||
const statusMap = {
|
||||
draft: 'Draft',
|
||||
active: 'Active',
|
||||
archived: 'Archived',
|
||||
error: 'Error',
|
||||
};
|
||||
|
||||
return statusMap[this.status] || this.status;
|
||||
}
|
||||
|
||||
@computed('status')
|
||||
get statusClass() {
|
||||
const statusClasses = {
|
||||
draft: 'status-draft',
|
||||
active: 'status-active',
|
||||
archived: 'status-archived',
|
||||
error: 'status-error',
|
||||
};
|
||||
|
||||
return statusClasses[this.status] || 'status-unknown';
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
countConditionsRecursively(conditions) {
|
||||
let count = 0;
|
||||
|
||||
if (!isArray(conditions)) {
|
||||
return count;
|
||||
}
|
||||
|
||||
conditions.forEach((condition) => {
|
||||
if (condition.conditions) {
|
||||
count += this.countConditionsRecursively(condition.conditions);
|
||||
} else {
|
||||
count++;
|
||||
}
|
||||
});
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
extractConditionsSummary(conditions, summary = []) {
|
||||
if (!isArray(conditions)) {
|
||||
return summary;
|
||||
}
|
||||
|
||||
conditions.forEach((condition) => {
|
||||
if (condition.conditions) {
|
||||
this.extractConditionsSummary(condition.conditions, summary);
|
||||
} else if (condition.field && condition.operator) {
|
||||
summary.push({
|
||||
field: condition.field.label || condition.field.name,
|
||||
operator: condition.operator.label || condition.operator.value,
|
||||
value: condition.value,
|
||||
table: condition.field.table || this.tableName,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
// API methods for interacting with the new backend
|
||||
async execute() {
|
||||
const owner = getOwner(this);
|
||||
const fetch = owner.lookup('service:fetch');
|
||||
|
||||
try {
|
||||
const response = await fetch.post(this.id ? `reports/${this.id}/execute` : 'reports/execute-query', { query_config: this.query_config });
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async export(format = 'csv', options = {}) {
|
||||
const owner = getOwner(this);
|
||||
const fetch = owner.lookup('service:fetch');
|
||||
|
||||
try {
|
||||
const response = await fetch.post(`reports/${this.id}/export`, {
|
||||
format,
|
||||
options,
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async validate() {
|
||||
const owner = getOwner(this);
|
||||
const fetch = owner.lookup('service:fetch');
|
||||
|
||||
try {
|
||||
const response = await fetch.post('reports/validate-query', {
|
||||
query_config: this.query_config,
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async analyze() {
|
||||
const owner = getOwner(this);
|
||||
const fetch = owner.lookup('service:fetch');
|
||||
|
||||
try {
|
||||
const response = await fetch.post('reports/analyze-query', {
|
||||
query_config: this.query_config,
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Static methods for direct query operations
|
||||
static async executeQuery(queryConfig) {
|
||||
const owner = getOwner(this);
|
||||
const fetch = owner.lookup('service:fetch');
|
||||
|
||||
try {
|
||||
const response = await fetch.post('reports/execute-query', {
|
||||
query_config: queryConfig,
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async exportQuery(queryConfig, format = 'csv', options = {}) {
|
||||
const owner = getOwner(this);
|
||||
const fetch = owner.lookup('service:fetch');
|
||||
|
||||
try {
|
||||
const response = await fetch('reports/export-query', {
|
||||
query_config: queryConfig,
|
||||
format,
|
||||
options,
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async validateQuery(queryConfig) {
|
||||
const owner = getOwner(this);
|
||||
const fetch = owner.lookup('service:fetch');
|
||||
|
||||
try {
|
||||
const response = await fetch.post('reports/validate-query', { query_config: queryConfig });
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async analyzeQuery(queryConfig) {
|
||||
const owner = getOwner(this);
|
||||
const fetch = owner.lookup('service:fetch');
|
||||
|
||||
try {
|
||||
const response = await fetch.post('reports/analyze-query', { query_config: queryConfig });
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async getTables() {
|
||||
try {
|
||||
const { tables } = await fetch.get('reports/tables');
|
||||
return tables;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async getTableSchema(tableName) {
|
||||
const owner = getOwner(this);
|
||||
const fetch = owner.lookup('service:fetch');
|
||||
|
||||
try {
|
||||
const { schema } = await fetch.get(`reports/tables/${tableName}/schema`);
|
||||
return schema;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async getExportFormats() {
|
||||
const owner = getOwner(this);
|
||||
const fetch = owner.lookup('service:fetch');
|
||||
|
||||
try {
|
||||
const { formats } = await fetch.get('reports/export-formats');
|
||||
return formats;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Utility methods for frontend display
|
||||
getComplexityBadgeClass() {
|
||||
const complexityClasses = {
|
||||
simple: 'badge-success',
|
||||
moderate: 'badge-warning',
|
||||
complex: 'badge-danger',
|
||||
};
|
||||
|
||||
return complexityClasses[this.complexity] || 'badge-secondary';
|
||||
}
|
||||
|
||||
getPerformanceBadgeClass() {
|
||||
const performanceClasses = {
|
||||
fast: 'badge-success',
|
||||
moderate: 'badge-warning',
|
||||
slow: 'badge-danger',
|
||||
};
|
||||
|
||||
return performanceClasses[this.estimatedPerformance] || 'badge-secondary';
|
||||
}
|
||||
|
||||
getQuerySummary() {
|
||||
const parts = [];
|
||||
|
||||
parts.push(`${this.totalSelectedColumns} columns from ${this.tableLabel}`);
|
||||
|
||||
if (this.hasJoins) {
|
||||
parts.push(`${this.joinedTables.length} joins`);
|
||||
}
|
||||
|
||||
if (this.hasConditions) {
|
||||
parts.push(`${this.conditionsCount} conditions`);
|
||||
}
|
||||
|
||||
if (this.hasGrouping) {
|
||||
parts.push('grouped');
|
||||
}
|
||||
|
||||
if (this.hasSorting) {
|
||||
parts.push('sorted');
|
||||
}
|
||||
|
||||
if (this.hasLimit) {
|
||||
parts.push(`limited to ${this.query_config.limit} rows`);
|
||||
}
|
||||
|
||||
return parts.join(', ');
|
||||
}
|
||||
}
|
||||
5
console/app/serializers/alert.js
Normal file
5
console/app/serializers/alert.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import ApplicationSerializer from '@fleetbase/ember-core/serializers/application';
|
||||
import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest';
|
||||
|
||||
export default class AlertSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) {
|
||||
}
|
||||
4
console/app/serializers/report.js
Normal file
4
console/app/serializers/report.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import ApplicationSerializer from '@fleetbase/ember-core/serializers/application';
|
||||
import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest';
|
||||
|
||||
export default class ReportSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) {}
|
||||
@@ -67,17 +67,3 @@ body.console-admin-organizations-index-index .next-table-wrapper > table {
|
||||
body[data-theme='dark'] #boot-loader > .loader-container > .loading-message {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/** hotfix: ember-power-select-trigger broken padding after upgrade - @todo move to ember-ui */
|
||||
body.fleetbase-console .ember-power-select-trigger {
|
||||
padding-top: 0.5rem;
|
||||
padding-right: 2.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
|
||||
/** hotfix sidebar-panel-toggle light mode - @todo move to ember-ui */
|
||||
body[data-theme="light"] .next-sidebar .next-content-panel-wrapper .next-content-panel-container .next-content-panel > .next-content-panel-header.next-sidebar-panel-toggle:hover {
|
||||
background-color: rgba(59, 130, 246, 1);
|
||||
background-color: rgba(59, 130, 246, var(--tw-bg-opacity));
|
||||
}
|
||||
@@ -1,6 +1,15 @@
|
||||
{{page-title (t "app.name")}}
|
||||
<Layout::Container>
|
||||
<Layout::Header @brand={{@model}} @menuItems={{this.menuItems}} @organizationMenuItems={{this.organizationMenuItems}} @userMenuItems={{this.userMenuItems}} @onAction={{this.onAction}} @showSidebarToggle={{true}} @sidebarToggleEnabled={{this.sidebarToggleEnabled}} @onSidebarToggle={{this.onSidebarToggle}} />
|
||||
<Layout::Header
|
||||
@brand={{@model}}
|
||||
@menuItems={{this.menuItems}}
|
||||
@organizationMenuItems={{this.organizationMenuItems}}
|
||||
@userMenuItems={{this.userMenuItems}}
|
||||
@onAction={{this.onAction}}
|
||||
@showSidebarToggle={{true}}
|
||||
@sidebarToggleEnabled={{this.sidebarToggleEnabled}}
|
||||
@onSidebarToggle={{this.onSidebarToggle}}
|
||||
/>
|
||||
<Layout::Main>
|
||||
<Layout::Sidebar @onSetup={{this.setSidebarContext}}>
|
||||
<div class="next-sidebar-content-inner">
|
||||
@@ -11,13 +20,21 @@
|
||||
<Layout::Section>
|
||||
{{outlet}}
|
||||
</Layout::Section>
|
||||
<ResourceContextPanel />
|
||||
</Layout::Main>
|
||||
<Layout::MobileNavbar @brand={{@model}} @user={{this.user}} @organizations={{this.organizations}} @menuItems={{this.menuItems}} @extensions={{this.extensions}} @onAction={{this.onAction}} />
|
||||
<Layout::MobileNavbar
|
||||
@brand={{@model}}
|
||||
@user={{this.user}}
|
||||
@organizations={{this.organizations}}
|
||||
@menuItems={{this.menuItems}}
|
||||
@extensions={{this.extensions}}
|
||||
@onAction={{this.onAction}}
|
||||
/>
|
||||
</Layout::Container>
|
||||
<ChatContainer />
|
||||
<ConsoleWormhole />
|
||||
<ImpersonatorTray />
|
||||
{{!-- template-lint-disable no-potential-path-strings --}}
|
||||
{{! template-lint-disable no-potential-path-strings }}
|
||||
<RegistryYield @registry="@fleetbase/console" as |RegistryComponent|>
|
||||
<RegistryComponent @controller={{this}} />
|
||||
</RegistryYield>
|
||||
</RegistryYield>
|
||||
46
console/app/transforms/array.js
Normal file
46
console/app/transforms/array.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import Transform from '@ember-data/serializer/transform';
|
||||
import { isArray } from '@ember/array';
|
||||
|
||||
export default class ArrayTransform extends Transform {
|
||||
deserialize(serialized) {
|
||||
if (serialized === null || serialized === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (isArray(serialized)) {
|
||||
return serialized;
|
||||
}
|
||||
|
||||
if (typeof serialized !== 'string') {
|
||||
return Array.from(serialized);
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(serialized);
|
||||
} catch (e) {
|
||||
// Fallback: return empty array if parsing fails
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
serialize(deserialized) {
|
||||
if (deserialized === null || deserialized === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (isArray(deserialized)) {
|
||||
return deserialized;
|
||||
}
|
||||
|
||||
if (typeof deserialized !== 'string') {
|
||||
return Array.from(deserialized);
|
||||
}
|
||||
|
||||
// Fallback: attempt to parse if it’s a string
|
||||
try {
|
||||
return JSON.parse(deserialized);
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
36
console/app/transforms/object.js
Normal file
36
console/app/transforms/object.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import Transform from '@ember-data/serializer/transform';
|
||||
import isObject from '@fleetbase/ember-core/utils/is-object';
|
||||
|
||||
export default class ObjectTransform extends Transform {
|
||||
deserialize(serialized) {
|
||||
if (!serialized) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (isObject(serialized)) {
|
||||
return serialized;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(serialized);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
serialize(deserialized) {
|
||||
if (!deserialized) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (isObject(deserialized)) {
|
||||
return deserialized;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(deserialized);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
160
console/app/utils/router-refresh-patch.js
Normal file
160
console/app/utils/router-refresh-patch.js
Normal file
@@ -0,0 +1,160 @@
|
||||
import { debug } from '@ember/debug';
|
||||
/**
|
||||
* Fleetbase Router Refresh Bug Fix Utility
|
||||
*
|
||||
* This utility patches the Ember.js router refresh bug that causes
|
||||
* "missing params" errors when transitioning to nested routes with
|
||||
* dynamic segments while query parameters with refreshModel: true
|
||||
* are present.
|
||||
*
|
||||
* Bug: https://github.com/emberjs/ember.js/issues/19260
|
||||
*
|
||||
* @author Fleetbase Pte Ltd <hello@fleetbase.io>
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Applies the router refresh bug fix patch
|
||||
* @param {Application} application - The Ember application instance
|
||||
*/
|
||||
export function patchRouterRefresh(application) {
|
||||
if (!application || typeof application.lookup !== 'function') {
|
||||
debug('[Fleetbase Router Patch] Invalid application instance provided');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const router = application.lookup('router:main');
|
||||
|
||||
if (!router || !router._routerMicrolib) {
|
||||
debug('[Fleetbase Router Patch] Router not found or invalid');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if already patched
|
||||
if (router._routerMicrolib._fleetbaseRefreshPatched) {
|
||||
debug('[Fleetbase Router Patch] Already applied, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
const originalRefresh = router._routerMicrolib.refresh.bind(router._routerMicrolib);
|
||||
|
||||
router._routerMicrolib.refresh = function (pivotRoute) {
|
||||
const previousTransition = this.activeTransition;
|
||||
const state = previousTransition ? previousTransition[this.constructor.STATE_SYMBOL] : this.state;
|
||||
const routeInfos = state.routeInfos;
|
||||
|
||||
if (pivotRoute === undefined) {
|
||||
pivotRoute = routeInfos[0].route;
|
||||
}
|
||||
|
||||
const name = routeInfos[routeInfos.length - 1].name;
|
||||
const currentRouteInfo = routeInfos[routeInfos.length - 1];
|
||||
|
||||
// Extract current dynamic segment parameters
|
||||
const contexts = [];
|
||||
if (currentRouteInfo && currentRouteInfo.params) {
|
||||
const handlers = this.recognizer.handlersFor(name);
|
||||
const targetHandler = handlers[handlers.length - 1];
|
||||
|
||||
if (targetHandler && targetHandler.names && targetHandler.names.length > 0) {
|
||||
targetHandler.names.forEach((paramName) => {
|
||||
if (currentRouteInfo.params[paramName] !== undefined) {
|
||||
contexts.push(currentRouteInfo.params[paramName]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const NamedTransitionIntent = this.constructor.NamedTransitionIntent;
|
||||
const intent = new NamedTransitionIntent(
|
||||
this,
|
||||
name,
|
||||
pivotRoute,
|
||||
contexts, // Preserve dynamic segments instead of empty array
|
||||
this._changedQueryParams || state.queryParams
|
||||
);
|
||||
|
||||
const newTransition = this.transitionByIntent(intent, false);
|
||||
|
||||
if (previousTransition && previousTransition.urlMethod === 'replace') {
|
||||
newTransition.method(previousTransition.urlMethod);
|
||||
}
|
||||
|
||||
return newTransition;
|
||||
};
|
||||
|
||||
// Mark as patched
|
||||
router._routerMicrolib._fleetbaseRefreshPatched = true;
|
||||
|
||||
debug('[Fleetbase Router Patch] Successfully applied router refresh bug fix');
|
||||
} catch (error) {
|
||||
debug('[Fleetbase Router Patch] Failed to apply patch: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternative error suppression approach for cases where patching fails
|
||||
* @param {Application} application - The Ember application instance
|
||||
*/
|
||||
export function suppressRouterRefreshErrors(application) {
|
||||
if (!application) {
|
||||
debug('[Fleetbase Router Patch] Invalid application instance for error suppression');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Global error handler for unhandled promise rejections
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
const error = event.reason;
|
||||
if (error?.message?.includes("You didn't provide enough string/numeric parameters to satisfy all of the dynamic segments")) {
|
||||
debug('[Fleetbase Router Patch] Suppressed known Ember route refresh bug:', error.message);
|
||||
event.preventDefault(); // Prevent the error from being logged
|
||||
}
|
||||
});
|
||||
|
||||
// Ember.js error handler
|
||||
if (window.Ember) {
|
||||
const originalEmberError = window.Ember.onerror;
|
||||
|
||||
window.Ember.onerror = function (error) {
|
||||
if (error?.message?.includes("You didn't provide enough string/numeric parameters to satisfy all of the dynamic segments")) {
|
||||
debug('[Fleetbase Router Patch] Suppressed known Ember route refresh bug:', error.message);
|
||||
return; // Suppress the error
|
||||
}
|
||||
|
||||
// Let other errors through
|
||||
if (originalEmberError) {
|
||||
return originalEmberError(error);
|
||||
}
|
||||
throw error;
|
||||
};
|
||||
}
|
||||
|
||||
debug('[Fleetbase Router Patch] Error suppression handlers installed');
|
||||
} catch (error) {
|
||||
debug('[Fleetbase Router Patch] Failed to install error suppression: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function to apply the complete router fix
|
||||
* @param {Application} application - The Ember application instance
|
||||
* @param {Object} options - Configuration options
|
||||
* @param {boolean} options.suppressErrors - Whether to also install error suppression (default: true)
|
||||
*/
|
||||
export default function applyRouterFix(application, options = {}) {
|
||||
const { suppressErrors = true } = options;
|
||||
|
||||
debug('[Fleetbase Router Patch] Applying Ember router refresh bug fix...');
|
||||
|
||||
// Apply the main patch
|
||||
patchRouterRefresh(application);
|
||||
|
||||
// Optionally install error suppression as fallback
|
||||
if (suppressErrors) {
|
||||
suppressRouterRefreshErrors(application);
|
||||
}
|
||||
|
||||
debug('[Fleetbase Router Patch] Router fix application complete');
|
||||
}
|
||||
@@ -9,7 +9,7 @@ module.exports = {
|
||||
'./node_modules/@fleetbase+*/addon/**/*.{hbs,js}',
|
||||
'./node_modules/@fleetbase/ember-ui/addon/templates/**/*.{hbs,js}',
|
||||
'./node_modules/@fleetbase/ember-ui/addon/components/**/*.{hbs,js}',
|
||||
'./node_modules/**/*-engine/addon/**/*.{hbs,js}'
|
||||
'./node_modules/**/*-engine/addon/**/*.{hbs,js}',
|
||||
],
|
||||
},
|
||||
safelist: [
|
||||
|
||||
14
console/tests/unit/models/alert-test.js
Normal file
14
console/tests/unit/models/alert-test.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { module, test } from 'qunit';
|
||||
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Model | alert', 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('alert', {});
|
||||
assert.ok(model);
|
||||
});
|
||||
});
|
||||
14
console/tests/unit/models/report-test.js
Normal file
14
console/tests/unit/models/report-test.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { module, test } from 'qunit';
|
||||
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Model | report', 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('report', {});
|
||||
assert.ok(model);
|
||||
});
|
||||
});
|
||||
24
console/tests/unit/serializers/alert-test.js
Normal file
24
console/tests/unit/serializers/alert-test.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { module, test } from 'qunit';
|
||||
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Serializer | alert', 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('alert');
|
||||
|
||||
assert.ok(serializer);
|
||||
});
|
||||
|
||||
test('it serializes records', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let record = store.createRecord('alert', {});
|
||||
|
||||
let serializedRecord = record.serialize();
|
||||
|
||||
assert.ok(serializedRecord);
|
||||
});
|
||||
});
|
||||
24
console/tests/unit/serializers/report-test.js
Normal file
24
console/tests/unit/serializers/report-test.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { module, test } from 'qunit';
|
||||
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Serializer | report', 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('report');
|
||||
|
||||
assert.ok(serializer);
|
||||
});
|
||||
|
||||
test('it serializes records', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let record = store.createRecord('report', {});
|
||||
|
||||
let serializedRecord = record.serialize();
|
||||
|
||||
assert.ok(serializedRecord);
|
||||
});
|
||||
});
|
||||
13
console/tests/unit/transforms/array-test.js
Normal file
13
console/tests/unit/transforms/array-test.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { module, test } from 'qunit';
|
||||
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Transform | array', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let transform = this.owner.lookup('transform:array');
|
||||
assert.ok(transform);
|
||||
});
|
||||
});
|
||||
13
console/tests/unit/transforms/object-test.js
Normal file
13
console/tests/unit/transforms/object-test.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { module, test } from 'qunit';
|
||||
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Transform | object', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let transform = this.owner.lookup('transform:object');
|
||||
assert.ok(transform);
|
||||
});
|
||||
});
|
||||
10
console/tests/unit/utils/router-refresh-patch-test.js
Normal file
10
console/tests/unit/utils/router-refresh-patch-test.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import routerRefreshPatch from '@fleetbase/console/utils/router-refresh-patch';
|
||||
import { module, test } from 'qunit';
|
||||
|
||||
module('Unit | Utility | router-refresh-patch', function () {
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it works', function (assert) {
|
||||
let result = routerRefreshPatch();
|
||||
assert.ok(result);
|
||||
});
|
||||
});
|
||||
@@ -76,6 +76,8 @@ common:
|
||||
push-notifications: Push Notifications
|
||||
role: Role
|
||||
timezone: Timezone
|
||||
bulk-delete: Bulk Delete
|
||||
bulk-delete-resource: Delete {resource}
|
||||
component:
|
||||
file:
|
||||
dropdown-label: File actions
|
||||
|
||||
Reference in New Issue
Block a user