@fleetbase/fleetops-engine
Version:
Fleet & Transport Management Extension for Fleetbase
602 lines (490 loc) • 18.2 kB
JavaScript
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { task } from 'ember-concurrency';
import copyToClipboard from '@fleetbase/ember-core/utils/copy-to-clipboard';
const SENSITIVE_METADATA_KEYS = ['password', 'secret', 'session', 'token'];
export default class TelematicFormComponent extends Component {
fetch;
notifications;
providers = [];
selectedProvider = this.args.resource?.provider_descriptor ?? null;
connectionTestResult;
showConnectionDiagnostics = false;
lastConnectionTestStartedAt;
lastConnectionTestCompletedAt;
activeStep = 0;
initialProviderApplied = false;
get setupSteps() {
return [
{
icon: 'plug',
label: 'Provider',
complete: this.activeStep > 0 && this.isProviderStepValid,
active: this.activeStep === 0,
},
{
icon: 'key',
label: 'Credentials',
complete: this.activeStep > 1 && this.areCredentialsValid,
active: this.activeStep === 1,
},
{
icon: 'circle-check',
label: 'Test',
complete: this.activeStep > 2 && this.isTestStepValid,
active: this.activeStep === 2,
},
{
icon: 'link',
label: 'Webhook',
complete: this.activeStep > 3 && this.isIntegrationStepValid,
active: this.activeStep === 3,
},
{
icon: 'clipboard-check',
label: 'Review',
complete: false,
active: this.activeStep === 4,
},
];
}
get providerCards() {
return this.providers.map((provider) => ({
...provider,
selected: this.selectedProvider?.key === provider.key,
}));
}
get hasCredentialValues() {
const credentials = this.args.resource?.credentials ?? {};
return Object.values(credentials).some((value) => value !== null && value !== undefined && value !== '');
}
get requiredCredentialFields() {
return (this.selectedProvider?.required_fields ?? []).filter((field) => field.required);
}
get credentialFields() {
return (this.selectedProvider?.required_fields ?? []).filter((field) => !field.advanced && !field.is_endpoint);
}
get advancedCredentialFields() {
return (this.selectedProvider?.required_fields ?? []).filter((field) => field.advanced || field.is_endpoint);
}
get hasAdvancedCredentialFields() {
return this.advancedCredentialFields.length > 0;
}
get missingCredentialFields() {
const credentials = this.args.resource?.credentials ?? {};
return this.requiredCredentialFields.filter((field) => {
const value = credentials[field.name];
return value === null || value === undefined || value === '';
});
}
get isProviderStepValid() {
return Boolean(this.selectedProvider);
}
get areCredentialsValid() {
return this.isProviderStepValid && this.missingCredentialFields.length === 0;
}
get isTestStepValid() {
return this.areCredentialsValid;
}
get isIntegrationStepValid() {
return Boolean(this.args.resource?.name);
}
get isReviewStepValid() {
return this.isProviderStepValid && this.areCredentialsValid && this.isIntegrationStepValid;
}
get isLastStep() {
return this.activeStep === this.setupSteps.length - 1;
}
get canGoBack() {
return this.activeStep > 0;
}
get primaryFooterLabel() {
if (this.isLastStep) {
return this.args.isSaving ? this.args.savingLabel : this.args.saveLabel;
}
return 'Continue';
}
get primaryFooterIcon() {
return this.isLastStep ? 'check' : 'arrow-right';
}
get canUsePrimaryAction() {
if (this.args.isSaving) {
return false;
}
if (this.isLastStep) {
return this.isReviewStepValid;
}
return this.canLeaveStep(this.activeStep);
}
get webhookUrl() {
const url = this.selectedProvider?.webhook_url;
const id = this.args.resource?.public_id;
if (!url || !id) {
return null;
}
const separator = url.includes('?') ? '&' : '?';
return `${url}${separator}telematic=${id}`;
}
get connectionState() {
if (this.testConnection.isRunning) {
return 'testing';
}
if (!this.connectionTestResult) {
return 'idle';
}
return this.connectionTestResult.success ? 'success' : 'failed';
}
get connectionStateTitle() {
if (this.connectionState === 'testing') {
return 'Testing provider credentials...';
}
if (this.connectionState === 'success') {
return 'Connection verified';
}
if (this.connectionState === 'failed') {
return 'Connection failed';
}
return 'Ready to test';
}
get connectionStateMessage() {
if (this.connectionState === 'testing') {
return 'Sending the configured credentials to the provider and waiting for a response.';
}
if (this.connectionState === 'idle') {
return 'Run a connection test to verify credentials before saving. You can still continue if the provider does not support live credential testing.';
}
if (this.isDuplicateConnectionMessage) {
return null;
}
return this.connectionTestResult?.message;
}
get isDuplicateConnectionMessage() {
const message = this.connectionTestResult?.message;
if (!message) {
return false;
}
return ['connection successful', 'connection verified'].includes(message.trim().toLowerCase());
}
get connectionMetadataEntries() {
const metadata = this.connectionTestResult?.metadata ?? {};
return Object.entries(metadata)
.filter(([key, value]) => {
if (value === null || value === undefined || value === '') {
return false;
}
return !this.isSensitiveMetadataKey(key);
})
.map(([key, value]) => ({
label: this.formatMetadataLabel(key),
value: this.formatDiagnosticValue(value),
}));
}
get hasConnectionMetadata() {
return this.connectionMetadataEntries.length > 0;
}
get connectionDiagnosticEntries() {
const entries = [];
const providerLabel = this.selectedProvider?.label ?? 'No provider selected';
const providerKey = this.selectedProvider?.key;
const startedAt = this.lastConnectionTestStartedAt;
const completedAt = this.lastConnectionTestCompletedAt;
entries.push({
time: this.formatDiagnosticTime(startedAt),
tone: 'muted',
text: `Provider selected: ${providerLabel}${providerKey ? ` (${providerKey})` : ''}`,
});
if (this.areCredentialsValid) {
entries.push({
time: this.formatDiagnosticTime(startedAt),
tone: 'muted',
text: `Credentials prepared: ${this.requiredCredentialFields.length} required fields present`,
});
} else {
entries.push({
time: this.formatDiagnosticTime(startedAt),
tone: 'warning',
text: `Credentials incomplete: ${this.missingCredentialFields.length} required fields missing`,
});
}
if (this.connectionState === 'idle') {
entries.push({
time: this.formatDiagnosticTime(),
tone: 'muted',
text: 'Credential test has not been run yet',
});
return entries;
}
entries.push({
time: this.formatDiagnosticTime(startedAt),
tone: 'info',
text: 'Sending provider credential test request',
});
if (this.connectionState === 'testing') {
entries.push({
time: this.formatDiagnosticTime(),
tone: 'info',
text: 'Awaiting provider response',
});
return entries;
}
entries.push({
time: this.formatDiagnosticTime(completedAt),
tone: 'info',
text: 'Provider response received',
});
if (this.hasConnectionMetadata) {
entries.push({
time: this.formatDiagnosticTime(completedAt),
tone: 'muted',
text: `Provider metadata: ${this.connectionMetadataEntries.map((entry) => `${entry.label}=${entry.value}`).join(', ')}`,
});
}
if (this.connectionState === 'success') {
entries.push({
time: this.formatDiagnosticTime(completedAt),
tone: 'success',
text: 'Connection verified',
});
return entries;
}
entries.push({
time: this.formatDiagnosticTime(completedAt),
tone: 'danger',
text: `Connection failed${this.connectionTestResult?.message ? `: ${this.formatDiagnosticValue(this.connectionTestResult.message)}` : ''}`,
});
return entries;
}
get hasConnectionDiagnostics() {
return this.connectionDiagnosticEntries.length > 0;
}
get connectionDiagnosticText() {
return this.connectionDiagnosticEntries.map((entry) => `[${entry.time}] ${entry.text}`).join('\n');
}
formatMetadataLabel(key) {
return key.replace(/_/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase());
}
formatDiagnosticValue(value) {
const sanitizedValue = this.sanitizeDiagnosticValue(value);
if (typeof sanitizedValue === 'object') {
return JSON.stringify(sanitizedValue);
}
return String(sanitizedValue);
}
sanitizeDiagnosticValue(value, seen = new WeakSet()) {
if (!value || typeof value !== 'object') {
return value;
}
if (seen.has(value)) {
return '[circular]';
}
seen.add(value);
if (Array.isArray(value)) {
return value.map((item) => this.sanitizeDiagnosticValue(item, seen));
}
return Object.entries(value).reduce((acc, [key, item]) => {
if (!this.isSensitiveMetadataKey(key)) {
acc[key] = this.sanitizeDiagnosticValue(item, seen);
}
return acc;
}, {});
}
formatDiagnosticTime(date = new Date()) {
if (!(date instanceof Date)) {
return '--:--:--';
}
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false });
}
isSensitiveMetadataKey(key) {
return SENSITIVE_METADATA_KEYS.some((sensitiveKey) => key.toLowerCase().includes(sensitiveKey));
}
resetConnectionTest() {
this.connectionTestResult = null;
this.lastConnectionTestStartedAt = null;
this.lastConnectionTestCompletedAt = null;
}
get credentialsActionButtons() {
return [
{
size: 'xs',
icon: 'plug',
text: 'Test Connection',
onClick: () => this.testConnection.perform(),
isLoading: this.testConnection.isRunning,
},
];
}
constructor() {
super(...arguments);
this.activeStep = this.selectedProvider ? 1 : 0;
this.loadProviders.perform();
}
setCredential(field, { target: { value } }) {
const credentials = this.args.resource.credentials ?? {};
this.args.resource.set('credentials', {
...credentials,
[field.name]: value,
});
this.resetConnectionTest();
}
selectProvider(provider) {
this.applyProvider(provider);
this.activeStep = 1;
}
applyProvider(provider) {
this.selectedProvider = provider;
this.resetConnectionTest();
this.args.resource.setProperties({
name: this.args.resource.name ?? provider.label,
provider: provider.key,
credentials: this.buildProviderCredentials(provider),
});
}
buildProviderCredentials(provider) {
return (provider.required_fields ?? []).reduce((acc, item) => {
acc[item.name] = item.default_value ?? null;
return acc;
}, {});
}
goToStep(index) {
if (!this.canReachStep(index)) {
return;
}
this.activeStep = index;
}
nextStep() {
if (!this.validateStep(this.activeStep)) {
return;
}
this.activeStep = Math.min(this.activeStep + 1, this.setupSteps.length - 1);
}
previousStep() {
this.activeStep = Math.max(this.activeStep - 1, 0);
}
primaryFooterAction() {
if (this.isLastStep) {
if (!this.validateStep(this.activeStep) || !this.isReviewStepValid) {
return;
}
return this.args.onSave?.();
}
return this.nextStep();
}
toggleConnectionDiagnostics() {
this.showConnectionDiagnostics = !this.showConnectionDiagnostics;
}
copyConnectionDiagnostics() {
copyToClipboard(this.connectionDiagnosticText)
.then(() => {
this.notifications.success('Connection diagnostics copied.');
})
.catch(() => {
this.notifications.error('Unable to copy connection diagnostics.');
});
}
canReachStep(index) {
if (index <= this.activeStep) {
return true;
}
for (let step = 0; step < index; step++) {
if (!this.canLeaveStep(step)) {
return false;
}
}
return true;
}
canLeaveStep(index) {
if (index === 0) {
return this.isProviderStepValid;
}
if (index === 1) {
return this.areCredentialsValid;
}
if (index === 3) {
return this.isIntegrationStepValid;
}
return true;
}
validateStep(index) {
if (index === 0 && !this.isProviderStepValid) {
this.notifications.warning('Choose a provider to continue.');
return false;
}
if (index === 1 && !this.areCredentialsValid) {
this.notifications.warning('Enter the required provider credentials to continue.');
return false;
}
if (index === 3 && !this.isIntegrationStepValid) {
this.notifications.warning('Enter an integration name to continue.');
return false;
}
if (index === 4 && !this.isReviewStepValid) {
this.notifications.warning('Complete the required setup steps before saving.');
return false;
}
return true;
}
applyInitialProvider() {
if (this.initialProviderApplied || !this.args.initialProviderKey || this.selectedProvider) {
return;
}
const provider = this.providers.find((candidate) => candidate.key === this.args.initialProviderKey);
if (!provider) {
return;
}
this.initialProviderApplied = true;
this.applyProvider(provider);
this.activeStep = 1;
}
*loadProviders() {
try {
const providers = yield this.fetch.get('telematics/providers');
this.providers = providers;
this.applyInitialProvider();
} catch (err) {
this.notifications.serverError(err);
}
}
*testConnection() {
this.lastConnectionTestStartedAt = new Date();
this.lastConnectionTestCompletedAt = null;
try {
const result = yield this.fetch.post(`telematics/${this.selectedProvider.key}/test-credentials`, this.getConnectionTestPayload());
this.connectionTestResult = result;
this.lastConnectionTestCompletedAt = new Date();
this.updateResourceConnectionTestMeta(result);
if (result.success) {
this.notifications.success('Connection successful!');
} else {
this.notifications.error(result.message);
}
} catch (error) {
this.lastConnectionTestCompletedAt = new Date();
this.connectionTestResult = {
success: false,
message: error.message || 'Connection test failed',
};
this.updateResourceConnectionTestMeta(this.connectionTestResult);
this.notifications.error('Connection test failed');
}
}
getConnectionTestPayload() {
return {
credentials: this.args.resource.credentials,
telematic_id: this.args.resource?.id,
};
}
updateResourceConnectionTestMeta(result) {
const meta = this.args.resource?.meta ?? {};
const testedAt = this.lastConnectionTestCompletedAt ?? new Date();
this.args.resource?.set('status', result?.success ? 'connected' : 'error');
this.args.resource?.set('meta', {
...meta,
last_connection_test: testedAt.toISOString(),
last_test_result: result?.success ? 'success' : 'failed',
last_error: result?.success ? null : (result?.message ?? 'Connection test failed'),
last_test_metadata: result?.metadata ?? {},
});
}
}