// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 /* eslint-disable */ // Import Amplify packages import { I18n } from 'aws-amplify'; import { Logger } from '@aws-amplify/core'; // Import packages import axios from 'axios'; // Import custom setting import { SortBy } from '../components/Enums'; // Import Subscription types for the application and various views import { AppSubscriptionTypes } from '../App'; import { ClientSubscriptionTypes } from '../views/Client'; import { EventSubscriptionTypes } from '../views/Event'; import { ObserverSubscriptionTypes } from '../views/Observer'; // Declare Amazon Virtual Andon console configuration declare var andon_config: any; // Global logging level export const LOGGING_LEVEL = 'DEBUG'; // Local constant variables const LOGGER = new Logger('CustomUtil', LOGGING_LEVEL); // File upload size limit - 10KB export const FILE_SIZE_LIMIT = 10 * 1024; /** * Custom error class. * @class CustomError */ export class CustomError extends Error { errorType: string; constructor(error: { errorType: string, message: string }) { super(error.message); this.errorType = error.errorType; } } /** * Send an anonymous metirc. * @param {object} data - Data to send an anonymous metric */ export async function sendMetrics(data: object) { if (andon_config.solutions_send_metrics === 'Yes') { const uuid = andon_config.solutions_solutionUuId; const body = { Data: { Resource: data, Region: andon_config.aws_project_region }, Version: andon_config.solutions_version, Solution: andon_config.solutions_solutionId, TimeStamp: `${new Date().toISOString().replace(/T/, ' ')}`, UUID: uuid }; const headers = { headers: { 'Content-Type': 'application/json' } }; try { await axios.post(andon_config.solutions_metrics_endpoint, body, headers); } catch (error) { LOGGER.error('Error in sending anonymous metrics.', error); } } } /** * Validate the general input string. * @param {string} input - String to validate * @param {number} minLength - Minimum input length * @param {number} maxLength - Maximum input length * @param {string} allowedSpecialCharacters - Allowed special characters * @return {boolean} Validated result */ export function validateGeneralInput(input: string, minLength: number, maxLength: number, allowedSpecialCharacters: string): boolean { if (input === '' && minLength === 0) { return true; } // Due to supporting multi-languages since v2.1, removed alphanumeric validation. if (input.length > maxLength || input.length < minLength || input.trimLeft().trimRight() === '') { return false; } let allowedSpecialCharactersCode: number[] = []; for (let i = 0, length = allowedSpecialCharacters.length; i < length; i++) { allowedSpecialCharactersCode.push(allowedSpecialCharacters.charCodeAt(i)); } if (allowedSpecialCharactersCode.length > 0) { for (let i = 0, length = input.length; i < length; i++) { let ch = input.charCodeAt(i); if ( (ch >= 32 && ch <= 47) || // [space] to "/" (ch >= 58 && ch <= 64) || // ":" to "@" (ch >= 91 && ch <= 96) || // "[" to "`" (ch >= 123 && ch <= 127) // "{" to the end of ASCII ) { if (!allowedSpecialCharactersCode.includes(ch)) { return false; } } } } return true; } /** * Validates a phone number * @param phoneNumber Phone number to validate * @param emptyStringIsValid Flag for whether an empty string should evaluate to true * @returns Boolean indicating whether the supplied string is a valid phone number */ export function validatePhoneNumber(phoneNumber: string, emptyStringIsValid = false): boolean { if (emptyStringIsValid && phoneNumber === '') { return true; } const regex = /^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$/; // NOSONAR: typescript:S4784 - Valid regex for phone number return validateUniqueListAndRegex(phoneNumber, ',', regex); } /** * Validate an E-Mail address * @param emailAddress E-Mail address to validate * @param emptyStringIsValid Flag for whether an empty string should evaluate to true * @returns Boolean indicating whether the supplied string is a valid email address */ export function validateEmailAddress(emailAddress: string, emptyStringIsValid = false): boolean { if (emptyStringIsValid && emailAddress === '') { return true; } const regex = /^[_a-z0-9-]+(\.[_a-z0-9-]+)*(\+[a-z0-9-]+)?@\w+([\.-]?\w+)*(\.\w{2,3})+$/; // NOSONAR: typescript:S4784 - Valid regex for email return validateUniqueListAndRegex(emailAddress, ',', regex); } /** * Splits `testStr` by the `delimiter` and returns whether each value passes the RegEx test * @param testStr The string with the list of strings to evaluate * @param delimiter The delimiter to split `testStr` by * @param regexStr The RegExp to validate each item in the testStr againist * @param trimWhitespace Boolean indicating whether each token after splitting `testStr` by the `delimiter` should be trimmed of whitespace * @returns booolean */ function validateUniqueListAndRegex(testStr: string, delimiter: string, regexStr: RegExp, trimWhitespace: boolean = true): boolean { // Split by the delimiter let splitTestStr = testStr.split(delimiter); if (trimWhitespace) { splitTestStr = splitTestStr.map(s => s.trim()); } for (const token of splitTestStr) { if (!regexStr.test(token)) { return false; } } // Ensure there are no duplicates if (new Set(splitTestStr).size !== splitTestStr.length) { return false; } return true; } /** * Add ISO time offset to the provided date. * @param {Date} date - Date to add ISO time offset * @return {string} ISO time offset added date */ export function addISOTimeOffset(date: Date): string { return new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString(); } /** * Get previous day. * @param {Date} date - Source date * @param {number} day - Days * @param {number} hours - Hours * @param {number} minutes - Minutes * @param {number} seconds - Seconds * @param {number} milliseconds - Milliseconds */ export function getPreviousDays(date: Date, day: number, hours: number, minutes: number, seconds: number, milliseconds: number): Date { return new Date( date.getFullYear(), date.getMonth(), date.getDate() + day, hours, minutes, seconds, milliseconds ); } /** * Convert seconds to HH hours MM minutes SS seconds format in selected language. * @param {number} seconds - Seconds to convert to hours, minutes, and seconds * @return {string} Converted format string */ export function convertSecondsToHms(seconds: number): string { if (seconds < 1) { return 'Just now'; } const hour = Math.floor(seconds / 3600); const minute = Math.floor(seconds % 3600 / 60); const second = Math.floor(seconds % 3600 % 60); const hourString = hour > 0 ? `${hour} ${I18n.get('text.time.hours')} ` : ''; const minuteString = minute > 0 ? `${minute} ${I18n.get('text.time.minutes')} ` : ''; const secondString = second > 0 ? `${second} ${I18n.get('text.time.seconds')}` : ''; return hourString.concat(minuteString, secondString); } /** * Sort a data ascending or descending order. * @param {any[]} data - Data to sort * @param {SoryBy} sortBy - Sort by ascending or descending * @param {string} sortKey - Sort key * @param {string | undefined} keyType - Sort key type * @return {any[]} Sorted data */ export function sortByName(data: any[], sortBy: SortBy, sortKey: string, keyType?: string): any[] { if (sortBy === SortBy.Asc) { if (keyType === 'number') { return data.sort((a, b) => (a[sortKey] ? a[sortKey] : 0) - (b[sortKey] ? b[sortKey] : 0)); } else { return data.sort((a, b) => (a[sortKey] ? a[sortKey] : '').localeCompare(b[sortKey] ? b[sortKey] : '')); } } else { if (keyType === 'number') { return data.sort((a, b) => (b[sortKey] ? b[sortKey] : 0) - (a[sortKey] ? a[sortKey] : 0)); } else { return data.sort((a, b) => (b[sortKey] ? b[sortKey] : '').localeCompare(a[sortKey] ? a[sortKey] : '')); } } } /** * Amplify provides some translated error messages, so some messages needs to be translated. * @param {string} message - Error message * @return {string} Custom error message */ export function getAmplifyCustomErrorMessage(message: string): string { if ( /Invalid.*password.*format/i.test(message) // NOSONAR: typescript:S4784 - Invalid password format || /validation.*error.*password.*constraint/i.test(message) // NOSONAR: typescript:S4784 - 1 validation error detected: Value at 'password' failed to satisfy constraint: Member must have length greater than or equal to 6 || /Password.*policy/i.test(message) // Password does not conform to policy: Password not long enough ) { message = 'error.cognito.password.policy'; } else if ( /Only.*radix.*supported/i.test(message) // NOSONAR: typescript:S4784 - Only radix 2, 4, 8, 16, 32 are supported || /validation.*error.*userName.*constraint/i.test(message) // NOSONAR: typescript:S4784 - 2 validation errors detected: Value '' at 'userName' failed to satisfy constraint: Member must satisfy regular expression pattern: [\p{L}\p{M}\p{S}\p{N}\p{P}]+; Value '' at 'userAlias' failed to satisfy constraint: Member must satisfy regular expression pattern: [\p{L}\p{M}\p{S}\p{N}\p{P}]+ ) { message = 'error.cognito.incorrect.username.password'; } return I18n.get(message); } /** * This returns the validation class name for input form. * @param {string} value - String value to check if it is empty * @param {boolean} isValid - Boolean value to check if the value is valid */ export function getInputFormValidationClassName(value: string, isValid: boolean): string { if (value === '') { return ''; } else { return isValid ? 'is-valid' : 'is-invalid'; } } /** * Make every data visible. * @param {any[]} data - Data to make visible */ export function makeAllVisible(data: any[]) { for (const datum of data) { datum.visible = true; } } /** * Make data visible when it matches search keyword. * @param {any[]} data - Data to filter by search keyword * @param {string} searchKey - Search key * @param {string} searchKeyword - Search keyword */ export function makeVisibleBySearchKeyword(data: any[], searchKey: string, searchKeyword: string) { for (const datum of data) { if (searchKeyword === '' || datum[searchKey].toLowerCase().includes(searchKeyword.toLowerCase())) { datum.visible = true; } else { datum.visible = false; } } } type SubscriptionTypes = AppSubscriptionTypes | ClientSubscriptionTypes | EventSubscriptionTypes | ObserverSubscriptionTypes; type ConfigureSubscriptionFn = (subscriptionType: any, delayMS: number) => Promise; /** * Will attempt to reestablish the subscription if the error was due to a socket closing. If the max delay (1000ms) is reached or there was a different error, the page will be reloaded * @param err The error that was caught from the subscription * @param subscriptionType The type of subscription * @param configureSubscriptionFn The function from within the App/View that will handle configuring the subscription * @param delayMS Amount of time to wait before reestablishing the subscription if the socket connection is lost */ export async function handleSubscriptionError(err: any, subscriptionType: SubscriptionTypes, configureSubscriptionFn: ConfigureSubscriptionFn, delayMS: number) { console.error(err); const MAX_DELAY_MS = 1000; if (delayMS > MAX_DELAY_MS) { // Exponential backoff has reached the limit we've defined; reload the page window.location.reload(); } else if (err.error && err.error.errors && err.error.errors[0] && err.error.errors[0].message === 'Connection closed') { // An AppSync subscription has lost its connection setTimeout(async () => { await configureSubscriptionFn(subscriptionType, delayMS * 2) }, delayMS); } else if (delayMS <= MAX_DELAY_MS && err.error && typeof err.error === 'string' && err.error.toLowerCase().includes('disconnected')) { // An IoT subscription has lost its connection setTimeout(async () => { await configureSubscriptionFn(subscriptionType, delayMS * 2) }, delayMS); } else { // Otherwise, reload the page window.location.reload(); } }