├── .editorconfig ├── .env.example ├── .erb ├── configs │ ├── .eslintrc │ ├── webpack.config.base.ts │ ├── webpack.config.eslint.ts │ ├── webpack.config.main.prod.ts │ ├── webpack.config.preload.dev.ts │ ├── webpack.config.renderer.dev.dll.ts │ ├── webpack.config.renderer.dev.ts │ ├── webpack.config.renderer.prod.ts │ └── webpack.paths.ts ├── img │ ├── erb-banner.svg │ └── erb-logo.png ├── mocks │ └── fileMock.js └── scripts │ ├── .eslintrc │ ├── check-build-exists.ts │ ├── check-native-dep.js │ ├── check-node-env.js │ ├── check-port-in-use.js │ ├── clean.js │ ├── delete-source-maps.js │ ├── electron-rebuild.js │ ├── link-modules.ts │ └── notarize.js ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── LICENSE ├── README.en.md ├── README.md ├── assets ├── assets.d.ts ├── entitlements.mac.plist ├── icon.icns ├── icon.ico ├── icon.png ├── icon.svg ├── icons │ ├── 1024x1024.png │ ├── 128x128.png │ ├── 16x16.png │ ├── 24x24.png │ ├── 256x256.png │ ├── 32x32.png │ ├── 48x48.png │ ├── 512x512.png │ └── 64x64.png └── resources │ ├── error.png │ ├── success.png │ └── warning.png ├── docs └── img │ ├── DatabaseView.png │ ├── DetailedMemoryView.png │ ├── EnvironmentView_Dark.png │ ├── EnvironmentView_English.png │ ├── EnvironmentView_Unavailable.png │ ├── EnvironmentView_White.png │ ├── HomeView.png │ └── banner.png ├── jest.config.js ├── package.json ├── prisma ├── migrations │ ├── 20221124203300_create_initial_tables_0_2_0 │ │ └── migration.sql │ ├── 20221205230300_create_resource_type_field │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── release └── app │ ├── package.json │ └── yarn.lock ├── scripts ├── clearDevLogs.ts ├── copySchema.ts └── sql │ └── generateHttpResponses.ts ├── src ├── common │ ├── classes │ │ ├── AuthKeysDecoder.ts │ │ ├── AuthKeysEncoder.ts │ │ └── FluigAPIClient.ts │ ├── i18n │ │ ├── i18n.ts │ │ └── resources │ │ │ ├── en.json │ │ │ ├── languageResources.ts │ │ │ └── pt.json │ ├── interfaces │ │ ├── AuthKeysControllerInterface.ts │ │ ├── AuthObject.ts │ │ ├── EnvironmentControllerInterface.ts │ │ ├── FluigVersionApiInterface.ts │ │ ├── HttpResponseResourceTypes.ts │ │ ├── MonitorHistoryInterface.ts │ │ └── UpdateScheduleControllerInterface.ts │ └── utils │ │ ├── byteSpeed.ts │ │ ├── compareSemver.ts │ │ ├── formatBytes.ts │ │ ├── parseBoolean.ts │ │ └── relativeTime.ts ├── main │ ├── classes │ │ └── AppUpdater.ts │ ├── controllers │ │ ├── AuthKeysController.ts │ │ ├── EnvironmentController.ts │ │ ├── HttpResponseController.ts │ │ ├── LanguageController.ts │ │ ├── LicenseHistoryController.ts │ │ ├── LogController.ts │ │ ├── MonitorHistoryController.ts │ │ ├── SettingsController.ts │ │ ├── StatisticsHistoryController.ts │ │ └── UpdateScheduleController.ts │ ├── database │ │ ├── migrationHandler.ts │ │ ├── prismaContext.ts │ │ └── seedDb.ts │ ├── interfaces │ │ ├── GitHubReleaseInterface.ts │ │ └── MigrationInterface.ts │ ├── main.ts │ ├── menu.ts │ ├── preload.ts │ ├── services │ │ ├── getEnvironmentRelease.ts │ │ ├── pingEnvironmentsJob.ts │ │ ├── syncEnvironmentsJob.ts │ │ └── validateOAuthPermission.ts │ └── utils │ │ ├── addIpcHandlers.ts │ │ ├── frequencyToMs.ts │ │ ├── fsUtils.ts │ │ ├── getAssetPath.ts │ │ ├── globalConstants.ts │ │ ├── logRotation.ts │ │ ├── logSystemConfigs.ts │ │ ├── resolveHtmlPath.ts │ │ ├── runPrismaCommand.ts │ │ └── trayBuilder.ts └── renderer │ ├── App.tsx │ ├── assets │ ├── img │ │ ├── banner_logo.png │ │ ├── database-logos │ │ │ ├── database.png │ │ │ ├── mysql.png │ │ │ ├── oracle.png │ │ │ └── sql-server.png │ │ ├── defaultServerLogo.png │ │ ├── logo.png │ │ ├── theme-preview-dark.png │ │ └── theme-preview-white.png │ ├── styles │ │ ├── components │ │ │ ├── CreateEnvironmentButton.scss │ │ │ ├── EnvironmentAvailabilityPanel.scss │ │ │ ├── EnvironmentListItem.scss │ │ │ ├── EnvironmentServerInfo.scss │ │ │ ├── EnvironmentServices.scss │ │ │ ├── FloatingNotification.scss │ │ │ ├── GraphTooltip.scss │ │ │ ├── Navbar.scss │ │ │ ├── ProgressBar.scss │ │ │ ├── RightButtons.scss │ │ │ ├── SmallTag.scss │ │ │ └── SpinnerLoader.scss │ │ ├── global.scss │ │ ├── pages │ │ │ ├── AppSettings.view.scss │ │ │ ├── EnvironmentView.scss │ │ │ └── HomeEnvironmentListView.scss │ │ └── utilities.scss │ └── svg │ │ └── color-server.svg │ ├── classes │ ├── EnvironmentFormValidator.ts │ └── FormValidator.ts │ ├── components │ ├── base │ │ ├── Box.tsx │ │ ├── CreateEnvironmentButton.tsx │ │ ├── DefaultMotionDiv.tsx │ │ ├── DynamicImageLoad.tsx │ │ ├── EnvironmentFavoriteButton.tsx │ │ ├── FloatingNotification.tsx │ │ ├── GraphTooltip.tsx │ │ ├── Loaders │ │ │ └── Spinner.tsx │ │ ├── ProgressBar.tsx │ │ ├── SmallTag.tsx │ │ ├── Stat │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ └── TimeIndicator.tsx │ ├── container │ │ ├── DatabaseNetworkGraph.tsx │ │ ├── DatabasePanel.tsx │ │ ├── DatabasePropsPanel.tsx │ │ ├── DatabaseStorageGraph.tsx │ │ ├── DiskPanel.tsx │ │ ├── EnvironmentAvailabilityPanel.tsx │ │ ├── EnvironmentLicensesPanel.tsx │ │ ├── EnvironmentName.tsx │ │ ├── EnvironmentPerformanceGraph.tsx │ │ ├── EnvironmentServerInfo.tsx │ │ ├── EnvironmentServicesPanel.tsx │ │ ├── HomeEnvironmentCard.tsx │ │ ├── MemoryPanel.tsx │ │ ├── Navbar │ │ │ ├── EnvironmentList.tsx │ │ │ ├── EnvironmentListItem.tsx │ │ │ ├── Logo.tsx │ │ │ ├── NavActionButtons.tsx │ │ │ └── Navbar.tsx │ │ └── SettingsPage │ │ │ ├── AboutSection.tsx │ │ │ ├── LanguageSettings.tsx │ │ │ ├── SystemTraySettings.tsx │ │ │ ├── ThemeSettings.tsx │ │ │ └── UpdatesSettings.tsx │ └── layout │ │ ├── EnvironmentArtifactsContainer.tsx │ │ ├── EnvironmentDatabaseContainer.tsx │ │ ├── EnvironmentDetailedMemoryContainer.tsx │ │ ├── EnvironmentInsightsContainer.tsx │ │ ├── EnvironmentRuntimeStatsContainer.tsx │ │ ├── EnvironmentServicesContainer.tsx │ │ └── EnvironmentSummaryContainer.tsx │ ├── contexts │ ├── EnvironmentListContext.tsx │ ├── NotificationsContext.tsx │ └── ThemeContext.tsx │ ├── index.ejs │ ├── index.tsx │ ├── ipc │ ├── environmentsIpcHandler.ts │ └── settingsIpcHandler.ts │ ├── pages │ ├── AppSettingsView.tsx │ ├── CreateEnvironmentView.tsx │ ├── EditEnvironmentSettingsView.tsx │ ├── EnvironmentView.tsx │ └── HomeEnvironmentListView.tsx │ ├── splash.html │ └── utils │ ├── getServiceName.ts │ └── globalContainerVariants.ts ├── test └── utils │ └── commonUtils.spec.ts ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Environment variables declared in this file are automatically made available to Prisma. 2 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema 3 | 4 | # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. 5 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings 6 | 7 | DATABASE_URL="file:./fluig-monitor.db" 8 | -------------------------------------------------------------------------------- /.erb/configs/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": "off", 4 | "global-require": "off", 5 | "import/no-dynamic-require": "off" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.base.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Base webpack config used across other specific configs 3 | */ 4 | 5 | import webpack from 'webpack'; 6 | import webpackPaths from './webpack.paths'; 7 | import { dependencies as externals } from '../../release/app/package.json'; 8 | 9 | export default { 10 | externals: [...Object.keys(externals || {})], 11 | 12 | stats: 'errors-only', 13 | 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.[jt]sx?$/, 18 | exclude: /node_modules/, 19 | use: { 20 | loader: 'ts-loader', 21 | options: { 22 | // Remove this line to enable type checking in webpack builds 23 | transpileOnly: true, 24 | }, 25 | }, 26 | }, 27 | ], 28 | }, 29 | 30 | output: { 31 | path: webpackPaths.srcPath, 32 | // https://github.com/webpack/webpack/issues/1114 33 | library: { 34 | type: 'commonjs2', 35 | }, 36 | }, 37 | 38 | /** 39 | * Determine the array of extensions that should be used to resolve modules. 40 | */ 41 | resolve: { 42 | extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'], 43 | modules: [webpackPaths.srcPath, 'node_modules'], 44 | }, 45 | 46 | plugins: [ 47 | new webpack.EnvironmentPlugin({ 48 | NODE_ENV: 'production', 49 | }), 50 | ], 51 | }; 52 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.eslint.ts: -------------------------------------------------------------------------------- 1 | /* eslint import/no-unresolved: off, import/no-self-import: off */ 2 | 3 | module.exports = require('./webpack.config.renderer.dev').default; 4 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.main.prod.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack config for production electron main process 3 | */ 4 | 5 | import path from 'path'; 6 | import webpack from 'webpack'; 7 | import { merge } from 'webpack-merge'; 8 | import TerserPlugin from 'terser-webpack-plugin'; 9 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 10 | import baseConfig from './webpack.config.base'; 11 | import webpackPaths from './webpack.paths'; 12 | import checkNodeEnv from '../scripts/check-node-env'; 13 | import deleteSourceMaps from '../scripts/delete-source-maps'; 14 | 15 | checkNodeEnv('production'); 16 | deleteSourceMaps(); 17 | 18 | const devtoolsConfig = 19 | process.env.DEBUG_PROD === 'true' 20 | ? { 21 | devtool: 'source-map', 22 | } 23 | : {}; 24 | 25 | export default merge(baseConfig, { 26 | ...devtoolsConfig, 27 | 28 | mode: 'production', 29 | 30 | target: 'electron-main', 31 | 32 | entry: { 33 | main: path.join(webpackPaths.srcMainPath, 'main.ts'), 34 | }, 35 | 36 | output: { 37 | path: webpackPaths.distMainPath, 38 | filename: '[name].js', 39 | }, 40 | 41 | optimization: { 42 | minimizer: [ 43 | new TerserPlugin({ 44 | parallel: true, 45 | }), 46 | ], 47 | }, 48 | 49 | plugins: [ 50 | new BundleAnalyzerPlugin({ 51 | analyzerMode: 52 | process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled', 53 | openAnalyzer: process.env.OPEN_ANALYZER === 'true', 54 | }), 55 | 56 | /** 57 | * Create global constants which can be configured at compile time. 58 | * 59 | * Useful for allowing different behaviour between development builds and 60 | * release builds 61 | * 62 | * NODE_ENV should be production so that modules do not perform certain 63 | * development checks 64 | */ 65 | new webpack.EnvironmentPlugin({ 66 | NODE_ENV: 'production', 67 | DEBUG_PROD: false, 68 | START_MINIMIZED: false, 69 | }), 70 | ], 71 | 72 | /** 73 | * Disables webpack processing of __dirname and __filename. 74 | * If you run the bundle in node.js it falls back to these values of node.js. 75 | * https://github.com/webpack/webpack/issues/2010 76 | */ 77 | node: { 78 | __dirname: false, 79 | __filename: false, 80 | }, 81 | }); 82 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.preload.dev.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | import { merge } from 'webpack-merge'; 4 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 5 | import baseConfig from './webpack.config.base'; 6 | import webpackPaths from './webpack.paths'; 7 | import checkNodeEnv from '../scripts/check-node-env'; 8 | 9 | // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's 10 | // at the dev webpack config is not accidentally run in a production environment 11 | if (process.env.NODE_ENV === 'production') { 12 | checkNodeEnv('development'); 13 | } 14 | 15 | const configuration: webpack.Configuration = { 16 | devtool: 'inline-source-map', 17 | 18 | mode: 'development', 19 | 20 | target: 'electron-preload', 21 | 22 | entry: path.join(webpackPaths.srcMainPath, 'preload.ts'), 23 | 24 | output: { 25 | path: webpackPaths.dllPath, 26 | filename: 'preload.js', 27 | }, 28 | 29 | plugins: [ 30 | new BundleAnalyzerPlugin({ 31 | analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', 32 | }), 33 | 34 | /** 35 | * Create global constants which can be configured at compile time. 36 | * 37 | * Useful for allowing different behaviour between development builds and 38 | * release builds 39 | * 40 | * NODE_ENV should be production so that modules do not perform certain 41 | * development checks 42 | * 43 | * By default, use 'development' as NODE_ENV. This can be overriden with 44 | * 'staging', for example, by changing the ENV variables in the npm scripts 45 | */ 46 | new webpack.EnvironmentPlugin({ 47 | NODE_ENV: 'development', 48 | }), 49 | 50 | new webpack.LoaderOptionsPlugin({ 51 | debug: true, 52 | }), 53 | ], 54 | 55 | /** 56 | * Disables webpack processing of __dirname and __filename. 57 | * If you run the bundle in node.js it falls back to these values of node.js. 58 | * https://github.com/webpack/webpack/issues/2010 59 | */ 60 | node: { 61 | __dirname: false, 62 | __filename: false, 63 | }, 64 | 65 | watch: true, 66 | }; 67 | 68 | export default merge(baseConfig, configuration); 69 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.renderer.dev.dll.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Builds the DLL for development electron renderer process 3 | */ 4 | 5 | import webpack from 'webpack'; 6 | import path from 'path'; 7 | import { merge } from 'webpack-merge'; 8 | import baseConfig from './webpack.config.base'; 9 | import webpackPaths from './webpack.paths'; 10 | import { dependencies } from '../../package.json'; 11 | import checkNodeEnv from '../scripts/check-node-env'; 12 | 13 | checkNodeEnv('development'); 14 | 15 | const dist = webpackPaths.dllPath; 16 | 17 | export default merge(baseConfig, { 18 | context: webpackPaths.rootPath, 19 | 20 | devtool: 'eval', 21 | 22 | mode: 'development', 23 | 24 | target: 'electron-renderer', 25 | 26 | externals: ['fsevents', 'crypto-browserify'], 27 | 28 | /** 29 | * Use `module` from `webpack.config.renderer.dev.js` 30 | */ 31 | module: require('./webpack.config.renderer.dev').default.module, 32 | 33 | entry: { 34 | renderer: Object.keys(dependencies || {}), 35 | }, 36 | 37 | output: { 38 | path: dist, 39 | filename: '[name].dev.dll.js', 40 | library: { 41 | name: 'renderer', 42 | type: 'var', 43 | }, 44 | }, 45 | 46 | plugins: [ 47 | new webpack.DllPlugin({ 48 | path: path.join(dist, '[name].json'), 49 | name: '[name]', 50 | }), 51 | 52 | /** 53 | * Create global constants which can be configured at compile time. 54 | * 55 | * Useful for allowing different behaviour between development builds and 56 | * release builds 57 | * 58 | * NODE_ENV should be production so that modules do not perform certain 59 | * development checks 60 | */ 61 | new webpack.EnvironmentPlugin({ 62 | NODE_ENV: 'development', 63 | }), 64 | 65 | new webpack.LoaderOptionsPlugin({ 66 | debug: true, 67 | options: { 68 | context: webpackPaths.srcPath, 69 | output: { 70 | path: webpackPaths.dllPath, 71 | }, 72 | }, 73 | }), 74 | ], 75 | }); 76 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.renderer.prod.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Build config for electron renderer process 3 | */ 4 | 5 | import path from 'path'; 6 | import webpack from 'webpack'; 7 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 8 | import MiniCssExtractPlugin from 'mini-css-extract-plugin'; 9 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 10 | import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'; 11 | import { merge } from 'webpack-merge'; 12 | import TerserPlugin from 'terser-webpack-plugin'; 13 | import baseConfig from './webpack.config.base'; 14 | import webpackPaths from './webpack.paths'; 15 | import checkNodeEnv from '../scripts/check-node-env'; 16 | import deleteSourceMaps from '../scripts/delete-source-maps'; 17 | 18 | checkNodeEnv('production'); 19 | deleteSourceMaps(); 20 | 21 | const devtoolsConfig = 22 | process.env.DEBUG_PROD === 'true' 23 | ? { 24 | devtool: 'source-map', 25 | } 26 | : {}; 27 | 28 | export default merge(baseConfig, { 29 | ...devtoolsConfig, 30 | 31 | mode: 'production', 32 | 33 | target: 'electron-renderer', 34 | 35 | entry: [ 36 | 'core-js', 37 | 'regenerator-runtime/runtime', 38 | path.join(webpackPaths.srcRendererPath, 'index.tsx'), 39 | ], 40 | 41 | output: { 42 | path: webpackPaths.distRendererPath, 43 | publicPath: './', 44 | filename: 'renderer.js', 45 | }, 46 | 47 | module: { 48 | rules: [ 49 | { 50 | test: /\.s?(a|c)ss$/, 51 | use: [ 52 | MiniCssExtractPlugin.loader, 53 | { 54 | loader: 'css-loader', 55 | options: { 56 | modules: true, 57 | sourceMap: true, 58 | importLoaders: 1, 59 | }, 60 | }, 61 | 'sass-loader', 62 | ], 63 | include: /\.module\.s?(c|a)ss$/, 64 | }, 65 | { 66 | test: /\.s?(a|c)ss$/, 67 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'], 68 | exclude: /\.module\.s?(c|a)ss$/, 69 | }, 70 | //Font Loader 71 | { 72 | test: /\.(woff|woff2|eot|ttf|otf)$/i, 73 | type: 'asset/resource', 74 | }, 75 | // SVG Font 76 | { 77 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 78 | use: { 79 | loader: 'url-loader', 80 | options: { 81 | limit: 10000, 82 | mimetype: 'image/svg+xml', 83 | }, 84 | }, 85 | }, 86 | // Common Image Formats 87 | { 88 | test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/, 89 | use: 'url-loader', 90 | }, 91 | ], 92 | }, 93 | 94 | optimization: { 95 | minimize: true, 96 | minimizer: [ 97 | new TerserPlugin({ 98 | parallel: true, 99 | }), 100 | new CssMinimizerPlugin(), 101 | ], 102 | }, 103 | 104 | plugins: [ 105 | /** 106 | * Create global constants which can be configured at compile time. 107 | * 108 | * Useful for allowing different behaviour between development builds and 109 | * release builds 110 | * 111 | * NODE_ENV should be production so that modules do not perform certain 112 | * development checks 113 | */ 114 | new webpack.EnvironmentPlugin({ 115 | NODE_ENV: 'production', 116 | DEBUG_PROD: false, 117 | }), 118 | 119 | new MiniCssExtractPlugin({ 120 | filename: 'style.css', 121 | }), 122 | 123 | new BundleAnalyzerPlugin({ 124 | analyzerMode: 125 | process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled', 126 | openAnalyzer: process.env.OPEN_ANALYZER === 'true', 127 | }), 128 | 129 | new HtmlWebpackPlugin({ 130 | filename: 'index.html', 131 | template: path.join(webpackPaths.srcRendererPath, 'index.ejs'), 132 | minify: { 133 | collapseWhitespace: true, 134 | removeAttributeQuotes: true, 135 | removeComments: true, 136 | }, 137 | isBrowser: false, 138 | isDevelopment: process.env.NODE_ENV !== 'production', 139 | }), 140 | 141 | new HtmlWebpackPlugin({ 142 | filename: 'splash.html', 143 | template: path.join(webpackPaths.srcRendererPath, 'splash.html'), 144 | }), 145 | ], 146 | }); 147 | -------------------------------------------------------------------------------- /.erb/configs/webpack.paths.ts: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const rootPath = path.join(__dirname, '../..'); 4 | 5 | const dllPath = path.join(__dirname, '../dll'); 6 | 7 | const srcPath = path.join(rootPath, 'src'); 8 | const srcMainPath = path.join(srcPath, 'main'); 9 | const srcRendererPath = path.join(srcPath, 'renderer'); 10 | 11 | const releasePath = path.join(rootPath, 'release'); 12 | const appPath = path.join(releasePath, 'app'); 13 | const appPackagePath = path.join(appPath, 'package.json'); 14 | const appNodeModulesPath = path.join(appPath, 'node_modules'); 15 | const srcNodeModulesPath = path.join(srcPath, 'node_modules'); 16 | 17 | const distPath = path.join(appPath, 'dist'); 18 | const distMainPath = path.join(distPath, 'main'); 19 | const distRendererPath = path.join(distPath, 'renderer'); 20 | 21 | const buildPath = path.join(releasePath, 'build'); 22 | 23 | export default { 24 | rootPath, 25 | dllPath, 26 | srcPath, 27 | srcMainPath, 28 | srcRendererPath, 29 | releasePath, 30 | appPath, 31 | appPackagePath, 32 | appNodeModulesPath, 33 | srcNodeModulesPath, 34 | distPath, 35 | distMainPath, 36 | distRendererPath, 37 | buildPath, 38 | }; 39 | -------------------------------------------------------------------------------- /.erb/img/erb-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizf-lf/fluig-monitor/23df4d7feb24e3de7a769f917bad9ee8d7892e71/.erb/img/erb-logo.png -------------------------------------------------------------------------------- /.erb/mocks/fileMock.js: -------------------------------------------------------------------------------- 1 | export default 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /.erb/scripts/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": "off", 4 | "global-require": "off", 5 | "import/no-dynamic-require": "off", 6 | "import/no-extraneous-dependencies": "off" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.erb/scripts/check-build-exists.ts: -------------------------------------------------------------------------------- 1 | // Check if the renderer and main bundles are built 2 | import path from 'path'; 3 | import chalk from 'chalk'; 4 | import fs from 'fs'; 5 | import webpackPaths from '../configs/webpack.paths'; 6 | 7 | const mainPath = path.join(webpackPaths.distMainPath, 'main.js'); 8 | const rendererPath = path.join(webpackPaths.distRendererPath, 'renderer.js'); 9 | 10 | if (!fs.existsSync(mainPath)) { 11 | throw new Error( 12 | chalk.whiteBright.bgRed.bold( 13 | 'The main process is not built yet. Build it by running "npm run build:main"' 14 | ) 15 | ); 16 | } 17 | 18 | if (!fs.existsSync(rendererPath)) { 19 | throw new Error( 20 | chalk.whiteBright.bgRed.bold( 21 | 'The renderer process is not built yet. Build it by running "npm run build:renderer"' 22 | ) 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /.erb/scripts/check-native-dep.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import chalk from 'chalk'; 3 | import { execSync } from 'child_process'; 4 | import { dependencies } from '../../package.json'; 5 | 6 | if (dependencies) { 7 | const dependenciesKeys = Object.keys(dependencies); 8 | const nativeDeps = fs 9 | .readdirSync('node_modules') 10 | .filter((folder) => fs.existsSync(`node_modules/${folder}/binding.gyp`)); 11 | if (nativeDeps.length === 0) { 12 | process.exit(0); 13 | } 14 | try { 15 | // Find the reason for why the dependency is installed. If it is installed 16 | // because of a devDependency then that is okay. Warn when it is installed 17 | // because of a dependency 18 | const { dependencies: dependenciesObject } = JSON.parse( 19 | execSync(`npm ls ${nativeDeps.join(' ')} --json`).toString() 20 | ); 21 | const rootDependencies = Object.keys(dependenciesObject); 22 | const filteredRootDependencies = rootDependencies.filter((rootDependency) => 23 | dependenciesKeys.includes(rootDependency) 24 | ); 25 | if (filteredRootDependencies.length > 0) { 26 | const plural = filteredRootDependencies.length > 1; 27 | console.log(` 28 | ${chalk.whiteBright.bgYellow.bold( 29 | 'Webpack does not work with native dependencies.' 30 | )} 31 | ${chalk.bold(filteredRootDependencies.join(', '))} ${ 32 | plural ? 'are native dependencies' : 'is a native dependency' 33 | } and should be installed inside of the "./release/app" folder. 34 | First, uninstall the packages from "./package.json": 35 | ${chalk.whiteBright.bgGreen.bold('npm uninstall your-package')} 36 | ${chalk.bold( 37 | 'Then, instead of installing the package to the root "./package.json":' 38 | )} 39 | ${chalk.whiteBright.bgRed.bold('npm install your-package')} 40 | ${chalk.bold('Install the package to "./release/app/package.json"')} 41 | ${chalk.whiteBright.bgGreen.bold('cd ./release/app && npm install your-package')} 42 | Read more about native dependencies at: 43 | ${chalk.bold( 44 | 'https://electron-react-boilerplate.js.org/docs/adding-dependencies/#module-structure' 45 | )} 46 | `); 47 | process.exit(1); 48 | } 49 | } catch (e) { 50 | console.log('Native dependencies could not be checked'); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.erb/scripts/check-node-env.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | export default function checkNodeEnv(expectedEnv) { 4 | if (!expectedEnv) { 5 | throw new Error('"expectedEnv" not set'); 6 | } 7 | 8 | if (process.env.NODE_ENV !== expectedEnv) { 9 | console.log( 10 | chalk.whiteBright.bgRed.bold( 11 | `"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config` 12 | ) 13 | ); 14 | process.exit(2); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.erb/scripts/check-port-in-use.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import detectPort from 'detect-port'; 3 | 4 | const port = process.env.PORT || '1212'; 5 | 6 | detectPort(port, (err, availablePort) => { 7 | if (port !== String(availablePort)) { 8 | throw new Error( 9 | chalk.whiteBright.bgRed.bold( 10 | `Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 npm start` 11 | ) 12 | ); 13 | } else { 14 | process.exit(0); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /.erb/scripts/clean.js: -------------------------------------------------------------------------------- 1 | import rimraf from 'rimraf'; 2 | import webpackPaths from '../configs/webpack.paths.ts'; 3 | import process from 'process'; 4 | 5 | const args = process.argv.slice(2); 6 | const commandMap = { 7 | dist: webpackPaths.distPath, 8 | release: webpackPaths.releasePath, 9 | dll: webpackPaths.dllPath, 10 | }; 11 | 12 | args.forEach((x) => { 13 | const pathToRemove = commandMap[x]; 14 | if (pathToRemove !== undefined) { 15 | rimraf.sync(pathToRemove); 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /.erb/scripts/delete-source-maps.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import rimraf from 'rimraf'; 3 | import webpackPaths from '../configs/webpack.paths'; 4 | 5 | export default function deleteSourceMaps() { 6 | rimraf.sync(path.join(webpackPaths.distMainPath, '*.js.map')); 7 | rimraf.sync(path.join(webpackPaths.distRendererPath, '*.js.map')); 8 | } 9 | -------------------------------------------------------------------------------- /.erb/scripts/electron-rebuild.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { execSync } from 'child_process'; 3 | import fs from 'fs'; 4 | import { dependencies } from '../../release/app/package.json'; 5 | import webpackPaths from '../configs/webpack.paths'; 6 | 7 | if ( 8 | Object.keys(dependencies || {}).length > 0 && 9 | fs.existsSync(webpackPaths.appNodeModulesPath) 10 | ) { 11 | const electronRebuildCmd = 12 | '../../node_modules/.bin/electron-rebuild --parallel --force --types prod,dev,optional --module-dir .'; 13 | const cmd = 14 | process.platform === 'win32' 15 | ? electronRebuildCmd.replace(/\//g, '\\') 16 | : electronRebuildCmd; 17 | execSync(cmd, { 18 | cwd: webpackPaths.appPath, 19 | stdio: 'inherit', 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /.erb/scripts/link-modules.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import webpackPaths from '../configs/webpack.paths'; 3 | 4 | const srcNodeModulesPath = webpackPaths.srcNodeModulesPath; 5 | const appNodeModulesPath = webpackPaths.appNodeModulesPath 6 | 7 | if (!fs.existsSync(srcNodeModulesPath) && fs.existsSync(appNodeModulesPath)) { 8 | fs.symlinkSync(appNodeModulesPath, srcNodeModulesPath, 'junction'); 9 | } 10 | -------------------------------------------------------------------------------- /.erb/scripts/notarize.js: -------------------------------------------------------------------------------- 1 | const { notarize } = require('electron-notarize'); 2 | const { build } = require('../../package.json'); 3 | 4 | exports.default = async function notarizeMacos(context) { 5 | const { electronPlatformName, appOutDir } = context; 6 | if (electronPlatformName !== 'darwin') { 7 | return; 8 | } 9 | 10 | if (!process.env.CI) { 11 | console.warn('Skipping notarizing step. Packaging is not running in CI'); 12 | return; 13 | } 14 | 15 | if (!('APPLE_ID' in process.env && 'APPLE_ID_PASS' in process.env)) { 16 | console.warn('Skipping notarizing step. APPLE_ID and APPLE_ID_PASS env variables must be set'); 17 | return; 18 | } 19 | 20 | const appName = context.packager.appInfo.productFilename; 21 | 22 | await notarize({ 23 | appBundleId: build.appId, 24 | appPath: `${appOutDir}/${appName}.app`, 25 | appleId: process.env.APPLE_ID, 26 | appleIdPassword: process.env.APPLE_ID_PASS, 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Coverage directory used by tools like istanbul 11 | coverage 12 | .eslintcache 13 | 14 | # Dependency directory 15 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 16 | node_modules 17 | 18 | # OSX 19 | .DS_Store 20 | 21 | release/app/dist 22 | release/build 23 | .erb/dll 24 | 25 | .idea 26 | npm-debug.log.* 27 | *.css.d.ts 28 | *.sass.d.ts 29 | *.scss.d.ts 30 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'erb', 3 | rules: { 4 | // A temporary hack related to IDE not resolving correct package.json 5 | 'import/no-extraneous-dependencies': 'off', 6 | // Since React 17 and typescript 4.1 you can safely disable the rule 7 | 'react/react-in-jsx-scope': 'off', 8 | }, 9 | parserOptions: { 10 | ecmaVersion: 2020, 11 | sourceType: 'module', 12 | project: './tsconfig.json', 13 | tsconfigRootDir: __dirname, 14 | createDefaultProgram: true, 15 | }, 16 | settings: { 17 | 'import/resolver': { 18 | // See https://github.com/benmosher/eslint-plugin-import/issues/1396#issuecomment-575727774 for line below 19 | node: {}, 20 | webpack: { 21 | config: require.resolve('./.erb/configs/webpack.config.eslint.ts'), 22 | }, 23 | }, 24 | 'import/parsers': { 25 | '@typescript-eslint/parser': ['.ts', '.tsx'], 26 | }, 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | *.exe binary 3 | *.png binary 4 | *.jpg binary 5 | *.jpeg binary 6 | *.ico binary 7 | *.icns binary 8 | *.eot binary 9 | *.otf binary 10 | *.ttf binary 11 | *.woff binary 12 | *.woff2 binary 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: 'BUG: ' 5 | labels: bug 6 | assignees: luizf-lf 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. Windows] 28 | - Version [e.g. 22] 29 | 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: 'Feature: ' 5 | labels: enhancement 6 | assignees: luizf-lf 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Coverage directory used by tools like istanbul 11 | coverage 12 | .eslintcache 13 | 14 | # Dependency directory 15 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 16 | node_modules 17 | 18 | # OSX 19 | .DS_Store 20 | 21 | release/app/dist 22 | release/build 23 | .erb/dll 24 | 25 | .idea 26 | npm-debug.log.* 27 | *.css.d.ts 28 | *.sass.d.ts 29 | *.scss.d.ts 30 | 31 | .env 32 | 33 | *.db 34 | *.db-journal 35 | 36 | src/main/generated 37 | 38 | prisma/ERD.svg 39 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "editorconfig.editorconfig", 5 | "prisma.prisma", 6 | "mikestead.dotenv", 7 | "esbenp.prettier-vscode" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Electron: Main", 6 | "type": "node", 7 | "request": "launch", 8 | "protocol": "inspector", 9 | "runtimeExecutable": "npm", 10 | "runtimeArgs": [ 11 | "run start:main --inspect=5858 --remote-debugging-port=9223" 12 | ], 13 | "preLaunchTask": "Start Webpack Dev" 14 | }, 15 | { 16 | "name": "Electron: Renderer", 17 | "type": "chrome", 18 | "request": "attach", 19 | "port": 9223, 20 | "webRoot": "${workspaceFolder}", 21 | "timeout": 15000 22 | } 23 | ], 24 | "compounds": [ 25 | { 26 | "name": "Electron: All", 27 | "configurations": ["Electron: Main", "Electron: Renderer"] 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | ".eslintrc": "jsonc", 4 | ".prettierrc": "jsonc", 5 | ".eslintignore": "ignore" 6 | }, 7 | 8 | "editor.formatOnSave": true, 9 | "javascript.validate.enable": false, 10 | "javascript.format.enable": false, 11 | "typescript.format.enable": false, 12 | 13 | "search.exclude": { 14 | ".git": true, 15 | ".eslintcache": true, 16 | ".erb/dll": true, 17 | "release/{build,app/dist}": true, 18 | "node_modules": true, 19 | "npm-debug.log.*": true, 20 | "test/**/__snapshots__": true, 21 | "package-lock.json": true, 22 | "*.{css,sass,scss}.d.ts": true 23 | }, 24 | "totvsLanguageServer.welcomePage": false, 25 | "cSpell.words": ["electronmon", "esnext", "Wifi"], 26 | "editor.defaultFormatter": "esbenp.prettier-vscode", 27 | "editor.rulers": [120] 28 | } 29 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "label": "Start Webpack Dev", 7 | "script": "start:renderer", 8 | "options": { 9 | "cwd": "${workspaceFolder}" 10 | }, 11 | "isBackground": true, 12 | "problemMatcher": { 13 | "owner": "custom", 14 | "pattern": { 15 | "regexp": "____________" 16 | }, 17 | "background": { 18 | "activeOnStart": true, 19 | "beginsPattern": "Compiling\\.\\.\\.$", 20 | "endsPattern": "(Compiled successfully|Failed to compile)\\.$" 21 | } 22 | } 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Electron React Boilerplate 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/assets.d.ts: -------------------------------------------------------------------------------- 1 | type Styles = Record; 2 | 3 | declare module '*.svg' { 4 | const content: string; 5 | export default content; 6 | } 7 | 8 | declare module '*.png' { 9 | const content: string; 10 | export default content; 11 | } 12 | 13 | declare module '*.jpg' { 14 | const content: string; 15 | export default content; 16 | } 17 | 18 | declare module '*.scss' { 19 | const content: Styles; 20 | export default content; 21 | } 22 | 23 | declare module '*.sass' { 24 | const content: Styles; 25 | export default content; 26 | } 27 | 28 | declare module '*.css' { 29 | const content: Styles; 30 | export default content; 31 | } 32 | -------------------------------------------------------------------------------- /assets/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-unsigned-executable-memory 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /assets/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizf-lf/fluig-monitor/23df4d7feb24e3de7a769f917bad9ee8d7892e71/assets/icon.icns -------------------------------------------------------------------------------- /assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizf-lf/fluig-monitor/23df4d7feb24e3de7a769f917bad9ee8d7892e71/assets/icon.ico -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizf-lf/fluig-monitor/23df4d7feb24e3de7a769f917bad9ee8d7892e71/assets/icon.png -------------------------------------------------------------------------------- /assets/icons/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizf-lf/fluig-monitor/23df4d7feb24e3de7a769f917bad9ee8d7892e71/assets/icons/1024x1024.png -------------------------------------------------------------------------------- /assets/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizf-lf/fluig-monitor/23df4d7feb24e3de7a769f917bad9ee8d7892e71/assets/icons/128x128.png -------------------------------------------------------------------------------- /assets/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizf-lf/fluig-monitor/23df4d7feb24e3de7a769f917bad9ee8d7892e71/assets/icons/16x16.png -------------------------------------------------------------------------------- /assets/icons/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizf-lf/fluig-monitor/23df4d7feb24e3de7a769f917bad9ee8d7892e71/assets/icons/24x24.png -------------------------------------------------------------------------------- /assets/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizf-lf/fluig-monitor/23df4d7feb24e3de7a769f917bad9ee8d7892e71/assets/icons/256x256.png -------------------------------------------------------------------------------- /assets/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizf-lf/fluig-monitor/23df4d7feb24e3de7a769f917bad9ee8d7892e71/assets/icons/32x32.png -------------------------------------------------------------------------------- /assets/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizf-lf/fluig-monitor/23df4d7feb24e3de7a769f917bad9ee8d7892e71/assets/icons/48x48.png -------------------------------------------------------------------------------- /assets/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizf-lf/fluig-monitor/23df4d7feb24e3de7a769f917bad9ee8d7892e71/assets/icons/512x512.png -------------------------------------------------------------------------------- /assets/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizf-lf/fluig-monitor/23df4d7feb24e3de7a769f917bad9ee8d7892e71/assets/icons/64x64.png -------------------------------------------------------------------------------- /assets/resources/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizf-lf/fluig-monitor/23df4d7feb24e3de7a769f917bad9ee8d7892e71/assets/resources/error.png -------------------------------------------------------------------------------- /assets/resources/success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizf-lf/fluig-monitor/23df4d7feb24e3de7a769f917bad9ee8d7892e71/assets/resources/success.png -------------------------------------------------------------------------------- /assets/resources/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizf-lf/fluig-monitor/23df4d7feb24e3de7a769f917bad9ee8d7892e71/assets/resources/warning.png -------------------------------------------------------------------------------- /docs/img/DatabaseView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizf-lf/fluig-monitor/23df4d7feb24e3de7a769f917bad9ee8d7892e71/docs/img/DatabaseView.png -------------------------------------------------------------------------------- /docs/img/DetailedMemoryView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizf-lf/fluig-monitor/23df4d7feb24e3de7a769f917bad9ee8d7892e71/docs/img/DetailedMemoryView.png -------------------------------------------------------------------------------- /docs/img/EnvironmentView_Dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizf-lf/fluig-monitor/23df4d7feb24e3de7a769f917bad9ee8d7892e71/docs/img/EnvironmentView_Dark.png -------------------------------------------------------------------------------- /docs/img/EnvironmentView_English.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizf-lf/fluig-monitor/23df4d7feb24e3de7a769f917bad9ee8d7892e71/docs/img/EnvironmentView_English.png -------------------------------------------------------------------------------- /docs/img/EnvironmentView_Unavailable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizf-lf/fluig-monitor/23df4d7feb24e3de7a769f917bad9ee8d7892e71/docs/img/EnvironmentView_Unavailable.png -------------------------------------------------------------------------------- /docs/img/EnvironmentView_White.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizf-lf/fluig-monitor/23df4d7feb24e3de7a769f917bad9ee8d7892e71/docs/img/EnvironmentView_White.png -------------------------------------------------------------------------------- /docs/img/HomeView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizf-lf/fluig-monitor/23df4d7feb24e3de7a769f917bad9ee8d7892e71/docs/img/HomeView.png -------------------------------------------------------------------------------- /docs/img/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizf-lf/fluig-monitor/23df4d7feb24e3de7a769f917bad9ee8d7892e71/docs/img/banner.png -------------------------------------------------------------------------------- /prisma/migrations/20221205230300_create_resource_type_field/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "HTTPResponse" ADD COLUMN "resourceType" TEXT; 3 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "sqlite" -------------------------------------------------------------------------------- /release/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fluig-monitor", 3 | "description": "Application for monitoring Fluig environments.", 4 | "version": "1.0.1", 5 | "main": "./dist/main/main.js", 6 | "author": { 7 | "name": "Luiz Ferreira", 8 | "email": "luizfernando_lf@hotmail.com.br", 9 | "url": "https://github.com/luizf-lf" 10 | }, 11 | "contributors": [ 12 | { 13 | "name": "Pablo Valle", 14 | "url": "https://github.com/pablooav" 15 | } 16 | ], 17 | "scripts": { 18 | "electron-rebuild": "node -r ts-node/register ../../.erb/scripts/electron-rebuild.js", 19 | "link-modules": "node -r ts-node/register ../../.erb/scripts/link-modules.ts", 20 | "postinstall": "npm run electron-rebuild && npm run link-modules" 21 | }, 22 | "license": "MIT" 23 | } -------------------------------------------------------------------------------- /release/app/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /scripts/clearDevLogs.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | /* eslint-disable no-console */ 3 | import path from 'path'; 4 | import * as fs from 'fs'; 5 | import formatBytes from '../src/common/utils/formatBytes'; 6 | 7 | const appData = process.env.APPDATA; 8 | 9 | console.log('🧹 Starting log file cleanup'); 10 | if (appData) { 11 | try { 12 | let clearedFiles = 0; 13 | const logPath = path.resolve(appData, 'fluig-monitor', 'logs'); 14 | console.log(`📂 Log file location: ${logPath}`); 15 | let totalSize = 0; 16 | 17 | if (fs.existsSync(logPath)) { 18 | fs.readdirSync(logPath).forEach((file) => { 19 | const stats = fs.statSync(path.resolve(logPath, file)); 20 | fs.rmSync(path.resolve(logPath, file)); 21 | 22 | totalSize += stats.size; 23 | clearedFiles += 1; 24 | }); 25 | 26 | if (clearedFiles > 0) { 27 | console.log(`✅ ${clearedFiles} log files have been deleted`); 28 | console.log( 29 | `🌌 A total of ${formatBytes( 30 | totalSize 31 | )} have been purged from this plane of reality` 32 | ); 33 | } else { 34 | console.log(`✅ There are no log files to be deleted`); 35 | } 36 | } 37 | } catch (e: any) { 38 | console.log('Could not clear log files: '); 39 | console.log(e.stack); 40 | } 41 | } else { 42 | console.log('❌ AppData folder could not be found'); 43 | } 44 | -------------------------------------------------------------------------------- /scripts/copySchema.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import path from 'path'; 3 | import fs from 'fs'; 4 | 5 | const source = path.resolve(__dirname, '../', 'prisma', 'schema.prisma'); 6 | const destination = path.resolve( 7 | __dirname, 8 | '../', 9 | 'release', 10 | 'app', 11 | 'dist', 12 | 'main', 13 | 'schema.prisma' 14 | ); 15 | 16 | console.log('📦 Copying prisma schema to build folder.'); 17 | 18 | fs.copyFileSync(source, destination); 19 | 20 | console.log(`✅ ${source} has been copied to ${destination}`); 21 | -------------------------------------------------------------------------------- /scripts/sql/generateHttpResponses.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { existsSync, rmSync, writeFileSync } from 'fs'; 3 | 4 | function randomNumber(min: number, max: number) { 5 | return Math.floor(Math.random() * (max - min + 1) + min); 6 | } 7 | 8 | function generateHttpResponses() { 9 | let statement = 10 | 'INSERT INTO HTTPResponse (id, environmentId, timestamp, endpoint, statusCode, statusMessage, responseTimeMs, resourceType) \n VALUES '; 11 | const sqlFile = './insert.sql'; 12 | const step = 15000; // 15s 13 | const now = Date.now(); 14 | let timestamp = new Date(Date.now() - 86400000).getTime(); 15 | let id = 11; // should be changed according to the last database id 16 | let counter = 0; 17 | 18 | while (timestamp < now) { 19 | statement += `(${id}, ${1}, ${timestamp}, 'http://mock.fluig.com/api/servlet/ping', 200, 'OK', ${randomNumber( 20 | 200, 21 | 250 22 | )}, 'PING'),\n`; 23 | 24 | id += 1; 25 | counter += 1; 26 | timestamp += step; 27 | } 28 | 29 | if (existsSync(sqlFile)) { 30 | rmSync(sqlFile); 31 | } 32 | 33 | writeFileSync(sqlFile, statement, { 34 | encoding: 'utf-8', 35 | }); 36 | 37 | console.log(`Generated ${counter} insert statements.`); 38 | } 39 | 40 | generateHttpResponses(); 41 | -------------------------------------------------------------------------------- /src/common/classes/AuthKeysDecoder.ts: -------------------------------------------------------------------------------- 1 | import * as forge from 'node-forge'; 2 | import log from 'electron-log'; 3 | import Store from 'electron-store'; 4 | import AuthObject from '../interfaces/AuthObject'; 5 | 6 | interface ConstructorProps { 7 | payload: string; 8 | hash: string; 9 | environmentId: number; 10 | secret?: string; 11 | } 12 | 13 | export default class AuthKeysDecoder { 14 | /** 15 | * the environment id referenced by the keys 16 | */ 17 | environmentId: number; 18 | 19 | /** 20 | * the payload string 21 | */ 22 | payload: string; 23 | 24 | /** 25 | * the decoder hash 26 | */ 27 | hash: string; 28 | 29 | /** 30 | * the decoder secret 31 | */ 32 | secret: string; 33 | 34 | /** 35 | * The decoded AuthObject 36 | */ 37 | decoded: AuthObject | null; 38 | 39 | constructor({ payload, hash, secret, environmentId }: ConstructorProps) { 40 | this.payload = payload; 41 | this.hash = hash; 42 | this.secret = secret || ''; 43 | this.environmentId = environmentId; 44 | this.decoded = { 45 | accessToken: '', 46 | consumerKey: '', 47 | consumerSecret: '', 48 | tokenSecret: '', 49 | }; 50 | } 51 | 52 | /** 53 | * decodes the auth object accordingly 54 | * @returns {AuthObject} the decoded AuthObject 55 | */ 56 | decode(): AuthObject | null { 57 | try { 58 | if (this.hash === 'json') { 59 | this.decoded = JSON.parse(this.payload); 60 | } else if (this.hash.indexOf('forge:') === 0) { 61 | this.secret = new Store().get( 62 | `envToken_${this.environmentId}` 63 | ) as string; 64 | const decipher = forge.cipher.createDecipher( 65 | 'AES-CBC', 66 | forge.util.decode64(this.hash.split('forge:')[1]) 67 | ); 68 | decipher.start({ iv: forge.util.decode64(this.secret) }); 69 | decipher.update( 70 | forge.util.createBuffer(forge.util.decode64(this.payload)) 71 | ); 72 | const result = decipher.finish(); 73 | 74 | if (result) { 75 | this.decoded = JSON.parse(decipher.output.data); 76 | return this.decoded; 77 | } 78 | this.decoded = null; 79 | } 80 | 81 | return this.decoded; 82 | } catch (error) { 83 | log.error('Could not decode the authentication keys:'); 84 | log.error(error); 85 | 86 | return null; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/common/classes/AuthKeysEncoder.ts: -------------------------------------------------------------------------------- 1 | import * as forge from 'node-forge'; 2 | import log from 'electron-log'; 3 | import AuthObject from '../interfaces/AuthObject'; 4 | 5 | interface EncryptedPayload { 6 | encrypted: string; 7 | key: string; 8 | iv: string; 9 | } 10 | 11 | export default class AuthKeysEncoder { 12 | /** 13 | * The pain oAuth object 14 | */ 15 | authObject: AuthObject; 16 | 17 | /** 18 | * The encrypted auth object as a string 19 | */ 20 | encryptedAuthObject: EncryptedPayload | null = null; 21 | 22 | /** 23 | * The verification hash string 24 | */ 25 | hashString: string = ''; 26 | 27 | constructor(auth: AuthObject) { 28 | this.authObject = auth; 29 | } 30 | 31 | encode(): EncryptedPayload | null { 32 | try { 33 | const key = forge.random.getBytesSync(32); 34 | const iv = forge.random.getBytesSync(32); 35 | 36 | const cipher = forge.cipher.createCipher('AES-CBC', key); 37 | cipher.start({ iv }); 38 | cipher.update(forge.util.createBuffer(JSON.stringify(this.authObject))); 39 | cipher.finish(); 40 | 41 | const encrypted = cipher.output.data; 42 | 43 | this.encryptedAuthObject = { 44 | encrypted: forge.util.encode64(String(encrypted)), 45 | key: forge.util.encode64(String(key)), 46 | iv: forge.util.encode64(String(iv)), 47 | }; 48 | 49 | return this.encryptedAuthObject; 50 | } catch (error) { 51 | log.error('Could not encode the authentication keys:'); 52 | log.error(error); 53 | 54 | return null; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/common/classes/FluigAPIClient.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import axios from 'axios'; 3 | import OAuth from 'oauth-1.0a'; 4 | import crypto from 'crypto'; 5 | import log from 'electron-log'; 6 | 7 | interface AuthKeys { 8 | consumerKey: string; 9 | consumerSecret: string; 10 | accessToken: string; 11 | tokenSecret: string; 12 | } 13 | 14 | interface RequestData { 15 | url: string; 16 | method: string; 17 | } 18 | 19 | interface ConstructorProps { 20 | oAuthKeys: AuthKeys; 21 | requestData: RequestData; 22 | } 23 | 24 | export default class FluigAPIClient { 25 | /** 26 | * The Http request status code (ex.: 200, 404, 500) 27 | */ 28 | httpStatus: number | null; 29 | 30 | /** 31 | * The http request status code message (Ex.: If it's 200: 'Ok', if it's 500: 'Internal Server Error') 32 | */ 33 | httpStatusText: string; 34 | 35 | /** 36 | * The http response data from the API (Usually a JSON) 37 | */ 38 | httpResponse: any; 39 | 40 | /** 41 | * The decoded auth keys passed as an argument on the constructor 42 | */ 43 | decodedKeys: AuthKeys; 44 | 45 | /** 46 | * The RequestData object containing the url endpoint and method (Currently only GET is supported) 47 | */ 48 | requestData: RequestData; 49 | 50 | /** 51 | * The oAuth helper from the oAuth-1.0a library 52 | */ 53 | oAuth: OAuth; 54 | 55 | /** 56 | * If the fluig client class has an error 57 | */ 58 | hasError: boolean; 59 | 60 | /** 61 | * The error stack, if the fluig client class has an error 62 | */ 63 | errorStack: string; 64 | 65 | constructor({ oAuthKeys, requestData }: ConstructorProps) { 66 | this.httpStatus = null; 67 | this.httpStatusText = ''; 68 | this.httpResponse = null; 69 | 70 | this.decodedKeys = oAuthKeys; 71 | this.requestData = requestData; 72 | 73 | this.oAuth = new OAuth({ 74 | consumer: { 75 | key: this.decodedKeys.consumerKey, 76 | secret: this.decodedKeys.consumerSecret, 77 | }, 78 | signature_method: 'HMAC-SHA1', 79 | hash_function(base_string, key) { 80 | return crypto 81 | .createHmac('sha1', key) 82 | .update(base_string) 83 | .digest('base64'); 84 | }, 85 | }); 86 | 87 | this.hasError = false; 88 | this.errorStack = ''; 89 | } 90 | 91 | /** 92 | * Makes a GET request 93 | * @param silent if the request should not write logs (defaults to false) 94 | */ 95 | async get(silent?: boolean, timeout?: number | null) { 96 | try { 97 | const token = { 98 | key: this.decodedKeys.accessToken, 99 | secret: this.decodedKeys.tokenSecret, 100 | }; 101 | 102 | if (!silent) { 103 | log.info('FluigAPIClient: GET endpoint', this.requestData.url); 104 | } 105 | 106 | const response = await axios.get(this.requestData.url, { 107 | headers: { 108 | ...this.oAuth.toHeader(this.oAuth.authorize(this.requestData, token)), 109 | }, 110 | timeout: timeout || 60000, // defaults the timeout to 60 seconds 111 | }); 112 | 113 | this.httpStatus = response.status; 114 | this.httpStatusText = response.statusText; 115 | this.httpResponse = response.data; 116 | } catch (e: any) { 117 | if (e.response) { 118 | this.httpStatus = e.response.status; 119 | this.httpStatusText = e.response.statusText; 120 | } 121 | this.hasError = true; 122 | this.errorStack = e.stack; 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/common/i18n/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import detector from 'i18next-browser-languagedetector'; 3 | import languageResources from './resources/languageResources'; 4 | 5 | // i18next native detection/caching will not be used, since saving the selected language to the database is easier 6 | i18n.use(detector).init({ 7 | resources: languageResources, 8 | interpolation: { 9 | escapeValue: false, 10 | }, 11 | fallbackLng: 'pt', 12 | debug: true, 13 | }); 14 | 15 | export default i18n; 16 | -------------------------------------------------------------------------------- /src/common/i18n/resources/languageResources.ts: -------------------------------------------------------------------------------- 1 | import pt from './pt.json'; 2 | import en from './en.json'; 3 | 4 | // using resources exported from local files since the 'i18next-fs-backend' package doesn't work properly. 5 | // it also solves the production build "error". 6 | const languageResources = { 7 | pt: { 8 | translation: pt, 9 | }, 10 | en: { 11 | translation: en, 12 | }, 13 | }; 14 | 15 | export default languageResources; 16 | -------------------------------------------------------------------------------- /src/common/interfaces/AuthKeysControllerInterface.ts: -------------------------------------------------------------------------------- 1 | export interface AuthKeysFormControllerInterface { 2 | payload: string; 3 | hash: string; 4 | } 5 | export interface AuthKeysControllerInterface 6 | extends AuthKeysFormControllerInterface { 7 | environmentId: number; 8 | } 9 | -------------------------------------------------------------------------------- /src/common/interfaces/AuthObject.ts: -------------------------------------------------------------------------------- 1 | export default interface AuthObject { 2 | consumerKey: string; 3 | consumerSecret: string; 4 | accessToken: string; 5 | tokenSecret: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/common/interfaces/EnvironmentControllerInterface.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Environment, 3 | EnvironmentAuthKeys, 4 | HTTPResponse, 5 | LicenseHistory, 6 | MonitorHistory, 7 | StatisticsHistory, 8 | UpdateSchedule, 9 | } from '../../main/generated/client'; 10 | 11 | export interface EnvironmentCreateControllerInterface { 12 | name: string; 13 | release: string; 14 | baseUrl: string; 15 | kind: string; 16 | logDeleted?: boolean; 17 | } 18 | 19 | export interface EnvironmentUpdateControllerInterface 20 | extends EnvironmentCreateControllerInterface { 21 | id: number; 22 | } 23 | 24 | export interface EnvironmentWithRelatedData extends Environment { 25 | updateScheduleId: UpdateSchedule | null; 26 | oAuthKeysId: EnvironmentAuthKeys | null; 27 | httpResponses: HTTPResponse[]; 28 | } 29 | 30 | export interface EnvironmentWithHistory extends Environment { 31 | licenseHistory: LicenseHistoryWithHttpResponse[]; 32 | statisticHistory: StatisticsHistoryWithHttpResponse[]; 33 | monitorHistory: MonitorHistoryWithHttpResponse[]; 34 | httpResponses: HTTPResponse[]; 35 | } 36 | 37 | export interface EnvironmentServerData extends Environment { 38 | statisticHistory: StatisticsHistoryWithHttpResponse[]; 39 | } 40 | 41 | export interface EnvironmentServices extends Environment { 42 | monitorHistory: MonitorHistoryWithHttpResponse[]; 43 | } 44 | 45 | export interface LicenseHistoryWithHttpResponse extends LicenseHistory { 46 | httpResponse: HTTPResponse; 47 | } 48 | 49 | export interface StatisticsHistoryWithHttpResponse extends StatisticsHistory { 50 | httpResponse: HTTPResponse; 51 | } 52 | 53 | export interface MonitorHistoryWithHttpResponse extends MonitorHistory { 54 | httpResponse: HTTPResponse; 55 | } 56 | 57 | export interface DetailedMemoryHistory { 58 | systemServerMemorySize: bigint; 59 | systemServerMemoryFree: bigint; 60 | memoryHeap: bigint; 61 | nonMemoryHeap: bigint; 62 | detailedMemory: string; 63 | systemHeapMaxSize: bigint; 64 | systemHeapSize: bigint; 65 | httpResponse: HTTPResponse; 66 | } 67 | 68 | export interface EnvironmentWithDetailedMemoryHistory extends Environment { 69 | statisticHistory: DetailedMemoryHistory[]; 70 | } 71 | -------------------------------------------------------------------------------- /src/common/interfaces/FluigVersionApiInterface.ts: -------------------------------------------------------------------------------- 1 | export interface FluigVersionApiInterface { 2 | content: string; 3 | message: string | null; 4 | } 5 | -------------------------------------------------------------------------------- /src/common/interfaces/HttpResponseResourceTypes.ts: -------------------------------------------------------------------------------- 1 | enum HttpResponseResourceType { 2 | LICENSES = 'LICENSES', 3 | MONITOR = 'MONITOR', 4 | STATISTICS = 'STATISTICS', 5 | PING = 'PING', 6 | } 7 | 8 | export default HttpResponseResourceType; 9 | -------------------------------------------------------------------------------- /src/common/interfaces/UpdateScheduleControllerInterface.ts: -------------------------------------------------------------------------------- 1 | export interface UpdateScheduleFormControllerInterface { 2 | scrapeFrequency: string; 3 | pingFrequency: string; 4 | } 5 | export interface UpdateScheduleControllerInterface 6 | extends UpdateScheduleFormControllerInterface { 7 | environmentId: number; 8 | } 9 | -------------------------------------------------------------------------------- /src/common/utils/byteSpeed.ts: -------------------------------------------------------------------------------- 1 | import log from 'electron-log'; 2 | import formatBytes from './formatBytes'; 3 | 4 | /** 5 | * Returns a string describing data speed in bytes per seconds 6 | * @param size total byte size 7 | * @param timer total time span in milliseconds 8 | * @returns a string describing the byte speed (eg.: 458KB/s) 9 | * @since 0.4.0 10 | */ 11 | export default function byteSpeed(size: number, timer: number): string { 12 | try { 13 | return `${formatBytes(size / (timer / 1000))}/s`; 14 | } catch (error) { 15 | log.error(`byteSpeed -> Could not determine the byte speed: ${error}`); 16 | return '0 Bytes/s'; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/common/utils/compareSemver.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Compares two strings of semantic versions (semver) and check which one is greater; 3 | * If the first is greater, returns 1, if the second is greater, returns -1, if both are equal, returns 0; 4 | * @since 0.5.0 5 | * @example 6 | * compareSemver('1.2.0', '1.2.3') => -1 7 | * compareSemver('1.2.5', '1.2.3') => 1 8 | * compareSemver('1.2.5', '1.2.5') => 0 9 | */ 10 | export default function compareSemver( 11 | version1: string, 12 | version2: string 13 | ): number { 14 | const semverToNumber = (version: string): number => { 15 | return Number( 16 | version 17 | .split('.') 18 | .map((item: string) => item.padStart(2, '0')) 19 | .join('') 20 | ); 21 | }; 22 | 23 | const version1Number = semverToNumber(version1); 24 | const version2Number = semverToNumber(version2); 25 | 26 | if (version1Number > version2Number) { 27 | return 1; 28 | } 29 | 30 | if (version1Number < version2Number) { 31 | return -1; 32 | } 33 | 34 | return 0; 35 | } 36 | -------------------------------------------------------------------------------- /src/common/utils/formatBytes.ts: -------------------------------------------------------------------------------- 1 | import log from 'electron-log'; 2 | 3 | /* eslint-disable no-restricted-properties */ 4 | /** 5 | * Formats a giver number of bytes to a human readable format. 6 | * @since 0.2 7 | * @param bytes number of bytes to format 8 | * @param decimals amount of decimals to use (defaults to 2) 9 | * @returns a string containing the formatted bytes in human readable format 10 | * @example formatBytes(1073741824) -> "1 GB" 11 | */ 12 | export default function formatBytes( 13 | bytes: number | null, 14 | decimals = 2 15 | ): string { 16 | try { 17 | if (!bytes || !+bytes) return '0 Bytes'; 18 | 19 | const k = 1024; 20 | const dm = decimals < 0 ? 0 : decimals; 21 | const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; 22 | 23 | const i = Math.floor(Math.log(bytes) / Math.log(k)); 24 | 25 | return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`; 26 | } catch (error) { 27 | log.error(`formatBytes -> Could not format bytes: ${error}`); 28 | return '0 Bytes'; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/common/utils/parseBoolean.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import log from 'electron-log'; 3 | 4 | /** 5 | * Parses a numeric or string value into a boolean value. 6 | * Will return false to unknown values; 7 | * @example 8 | * parseBoolean('true') => true 9 | * parseBoolean(0) => false 10 | * parseBoolean('sample') => false 11 | */ 12 | export default function parseBoolean(source: any): boolean { 13 | try { 14 | if (!['number', 'string'].includes(typeof source)) { 15 | return false; 16 | } 17 | 18 | if ([0, 'false', 'FALSE'].includes(source)) { 19 | return false; 20 | } 21 | 22 | if ([1, 'true', 'TRUE'].includes(source)) { 23 | return true; 24 | } 25 | 26 | return false; 27 | } catch (error) { 28 | log.error( 29 | `parseBoolean -> Could not parse the non boolean value: ${error}` 30 | ); 31 | return false; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/common/utils/relativeTime.ts: -------------------------------------------------------------------------------- 1 | import log from 'electron-log'; 2 | 3 | interface TimeBlocksObject { 4 | days: number; 5 | hours: number; 6 | minutes: number; 7 | seconds: number; 8 | } 9 | 10 | /** 11 | * transforms a given amount of seconds into a time block, in order to be used as a relative time string format 12 | * @example 13 | * relativeTime(90) => { days: 0, hours: 0, minutes: 1, seconds: 30 } 14 | * @since 0.1.2 15 | */ 16 | export default function relativeTime( 17 | totalSeconds: number 18 | ): TimeBlocksObject | null { 19 | try { 20 | /** 21 | * minutes to seconds = 60 22 | * hours to seconds = 3600 23 | * days to seconds = 86400 24 | */ 25 | 26 | let days = 0; 27 | let hours = 0; 28 | let minutes = 0; 29 | let seconds = 0; 30 | 31 | if (totalSeconds >= 60) { 32 | minutes = Math.floor(totalSeconds / 60); 33 | seconds = totalSeconds - minutes * 60; 34 | 35 | if (minutes >= 60) { 36 | hours = Math.floor(minutes / 60); 37 | minutes -= hours * 60; 38 | 39 | if (hours >= 24) { 40 | days = Math.floor(hours / 24); 41 | hours -= days * 24; 42 | } 43 | } 44 | } else { 45 | seconds = totalSeconds; 46 | } 47 | 48 | return { days, hours, minutes, seconds }; 49 | } catch (error) { 50 | log.error(`relativeTime -> Could not determine past period: ${error}`); 51 | return null; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/controllers/AuthKeysController.ts: -------------------------------------------------------------------------------- 1 | import log from 'electron-log'; 2 | import { EnvironmentAuthKeys } from '../generated/client'; 3 | import prismaClient from '../database/prismaContext'; 4 | import { AuthKeysControllerInterface } from '../../common/interfaces/AuthKeysControllerInterface'; 5 | 6 | export default class AuthKeysController { 7 | created: EnvironmentAuthKeys | null; 8 | 9 | updated: EnvironmentAuthKeys | null; 10 | 11 | constructor() { 12 | this.created = null; 13 | this.updated = null; 14 | } 15 | 16 | async new(data: AuthKeysControllerInterface): Promise { 17 | log.info('AuthKeysController: Creating a new environment auth keys.'); 18 | this.created = await prismaClient.environmentAuthKeys.create({ 19 | data, 20 | }); 21 | 22 | return this.created; 23 | } 24 | 25 | async update( 26 | data: AuthKeysControllerInterface 27 | ): Promise { 28 | log.info('AuthKeysController: Updating environment auth keys.'); 29 | 30 | this.updated = await prismaClient.environmentAuthKeys.update({ 31 | where: { 32 | id: data.environmentId, 33 | }, 34 | data, 35 | }); 36 | 37 | return this.updated; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/controllers/HttpResponseController.ts: -------------------------------------------------------------------------------- 1 | import log from 'electron-log'; 2 | import prismaClient from '../database/prismaContext'; 3 | import { HTTPResponse } from '../generated/client'; 4 | 5 | interface CreateHttpResponseProps { 6 | environmentId: number; 7 | statusCode: number; 8 | statusMessage?: string; 9 | endpoint?: string; 10 | resourceType?: string; 11 | timestamp: string; 12 | responseTimeMs: number; 13 | } 14 | 15 | export default class HttpResponseController { 16 | created: null | HTTPResponse; 17 | 18 | constructor() { 19 | this.created = null; 20 | } 21 | 22 | async new( 23 | data: CreateHttpResponseProps, 24 | silent?: boolean 25 | ): Promise { 26 | if (!silent) { 27 | log.info( 28 | 'HttpResponseController: Creating a new http response on the database' 29 | ); 30 | } 31 | this.created = await prismaClient.hTTPResponse.create({ 32 | data, 33 | }); 34 | 35 | return this.created; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/controllers/LanguageController.ts: -------------------------------------------------------------------------------- 1 | import log from 'electron-log'; 2 | import { AppSetting } from '../generated/client'; 3 | import prismaClient from '../database/prismaContext'; 4 | 5 | export default class LanguageController { 6 | language: string; 7 | 8 | updated: AppSetting | null; 9 | 10 | constructor() { 11 | this.language = 'pt'; 12 | this.updated = null; 13 | } 14 | 15 | async get(): Promise { 16 | log.info('LanguageController: Querying saved language from database'); 17 | const language = await prismaClient.appSetting.findFirst({ 18 | where: { settingId: 'APP_LANGUAGE' }, 19 | }); 20 | 21 | if (language !== null) { 22 | this.language = language.value; 23 | return this.language; 24 | } 25 | 26 | return 'pt'; 27 | } 28 | 29 | async update(language: string): Promise { 30 | log.info('LanguageController: Updating app language on the database'); 31 | 32 | this.updated = await prismaClient.appSetting.update({ 33 | where: { 34 | settingId: 'APP_LANGUAGE', 35 | }, 36 | data: { 37 | value: language, 38 | }, 39 | }); 40 | 41 | return this.updated; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/controllers/LicenseHistoryController.ts: -------------------------------------------------------------------------------- 1 | import log from 'electron-log'; 2 | import { HTTPResponse, LicenseHistory } from '../generated/client'; 3 | import prismaClient from '../database/prismaContext'; 4 | import HttpResponseController from './HttpResponseController'; 5 | import HttpResponseResourceType from '../../common/interfaces/HttpResponseResourceTypes'; 6 | 7 | interface LogLicenseProps { 8 | environmentId: number; 9 | statusCode: number; 10 | statusMessage: string; 11 | timestamp: string; 12 | endpoint?: string; 13 | responseTimeMs: number; 14 | licenseData: { 15 | activeUsers: number; 16 | remainingLicenses: number; 17 | tenantId: number; 18 | totalLicenses: number; 19 | }; 20 | } 21 | 22 | export interface EnvironmentLicenseData { 23 | id: number; 24 | activeUsers: number; 25 | remainingLicenses: number; 26 | totalLicenses: number; 27 | tenantId: number; 28 | httpResponse: HTTPResponse; 29 | } 30 | 31 | export default class LicenseHistoryController { 32 | created: LicenseHistory | null; 33 | 34 | constructor() { 35 | this.created = null; 36 | } 37 | 38 | async new({ 39 | environmentId, 40 | statusCode, 41 | statusMessage, 42 | timestamp, 43 | endpoint, 44 | responseTimeMs, 45 | licenseData, 46 | }: LogLicenseProps): Promise { 47 | const { activeUsers, remainingLicenses, tenantId, totalLicenses } = 48 | licenseData; 49 | const httpResponse = await new HttpResponseController().new({ 50 | environmentId, 51 | statusCode, 52 | statusMessage, 53 | endpoint, 54 | resourceType: HttpResponseResourceType.LICENSES, 55 | timestamp, 56 | responseTimeMs, 57 | }); 58 | 59 | log.info( 60 | 'LicenseHistoryController: Creating a new license history on the database' 61 | ); 62 | this.created = await prismaClient.licenseHistory.create({ 63 | data: { 64 | activeUsers, 65 | remainingLicenses, 66 | tenantId, 67 | totalLicenses, 68 | environmentId, 69 | httpResponseId: httpResponse.id, 70 | }, 71 | }); 72 | 73 | return this.created; 74 | } 75 | 76 | /** 77 | * Gets the latest license data from a given environment by id. 78 | * @since 0.5 79 | */ 80 | static async getLastLicenseData( 81 | environmentId: number 82 | ): Promise { 83 | const licenseData = await prismaClient.licenseHistory.findFirst({ 84 | select: { 85 | id: true, 86 | activeUsers: true, 87 | remainingLicenses: true, 88 | totalLicenses: true, 89 | tenantId: true, 90 | httpResponse: true, 91 | }, 92 | orderBy: { 93 | httpResponse: { 94 | timestamp: 'desc', 95 | }, 96 | }, 97 | where: { 98 | environmentId, 99 | }, 100 | }); 101 | 102 | return licenseData; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/controllers/LogController.ts: -------------------------------------------------------------------------------- 1 | import prismaClient from '../database/prismaContext'; 2 | import { Log } from '../generated/client'; 3 | 4 | interface LogCreateControllerInterface { 5 | type: string; 6 | message: string; 7 | timestamp?: Date; 8 | } 9 | 10 | export default class LogController { 11 | created: Log | null; 12 | 13 | constructor() { 14 | this.created = null; 15 | } 16 | 17 | async writeLog(data: LogCreateControllerInterface): Promise { 18 | this.created = await prismaClient.log.create({ data }); 19 | 20 | return this.created; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/controllers/MonitorHistoryController.ts: -------------------------------------------------------------------------------- 1 | import log from 'electron-log'; 2 | import prismaClient from '../database/prismaContext'; 3 | import { MonitorHistory } from '../generated/client'; 4 | import HttpResponseController from './HttpResponseController'; 5 | import HttpResponseResourceType from '../../common/interfaces/HttpResponseResourceTypes'; 6 | 7 | interface MonitorItem { 8 | name: string; 9 | status: string; 10 | } 11 | 12 | interface MonitorHistoryCreateProps { 13 | environmentId: number; 14 | statusCode: number; 15 | statusMessage: string; 16 | timestamp: string; 17 | responseTimeMs: number; 18 | endpoint?: string; 19 | monitorData: MonitorItem[]; 20 | } 21 | 22 | export default class MonitorHistoryController { 23 | created: MonitorHistory | null; 24 | 25 | constructor() { 26 | this.created = null; 27 | } 28 | 29 | async new(data: MonitorHistoryCreateProps): Promise { 30 | const analytics = data.monitorData.find( 31 | (i) => i.name === 'ANALYTICS_AVAIABILITY' 32 | )?.status; 33 | const licenseServer = data.monitorData.find( 34 | (i) => i.name === 'LICENSE_SERVER_AVAILABILITY' 35 | )?.status; 36 | const mailServer = data.monitorData.find( 37 | (i) => i.name === 'MAIL_SERVER_AVAILABILITY' 38 | )?.status; 39 | const MSOffice = data.monitorData.find( 40 | (i) => i.name === 'MS_OFFICE_AVAILABILITY' 41 | )?.status; 42 | const openOffice = data.monitorData.find( 43 | (i) => i.name === 'OPEN_OFFICE_AVAILABILITY' 44 | )?.status; 45 | const realTime = data.monitorData.find( 46 | (i) => i.name === 'REAL_TIME_AVAILABILITY' 47 | )?.status; 48 | const solrServer = data.monitorData.find( 49 | (i) => i.name === 'SOLR_SERVER_AVAILABILITY' 50 | )?.status; 51 | const viewer = data.monitorData.find( 52 | (i) => i.name === 'VIEWER_AVAILABILITY' 53 | )?.status; 54 | 55 | const httpResponse = await new HttpResponseController().new({ 56 | environmentId: data.environmentId, 57 | statusCode: data.statusCode, 58 | statusMessage: data.statusMessage, 59 | endpoint: data.endpoint, 60 | resourceType: HttpResponseResourceType.MONITOR, 61 | timestamp: data.timestamp, 62 | responseTimeMs: data.responseTimeMs, 63 | }); 64 | 65 | log.info( 66 | 'MonitorHistoryController: Creating a new monitor history on the database' 67 | ); 68 | this.created = await prismaClient.monitorHistory.create({ 69 | data: { 70 | environmentId: data.environmentId, 71 | httpResponseId: httpResponse.id, 72 | analytics, 73 | licenseServer, 74 | mailServer, 75 | MSOffice, 76 | openOffice, 77 | realTime, 78 | solrServer, 79 | viewer, 80 | }, 81 | }); 82 | 83 | return this.created; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/controllers/UpdateScheduleController.ts: -------------------------------------------------------------------------------- 1 | import log from 'electron-log'; 2 | import { UpdateSchedule } from '../generated/client'; 3 | import prismaClient from '../database/prismaContext'; 4 | import { UpdateScheduleControllerInterface } from '../../common/interfaces/UpdateScheduleControllerInterface'; 5 | 6 | export default class UpdateScheduleController { 7 | created: UpdateSchedule | null; 8 | 9 | updated: UpdateSchedule | null; 10 | 11 | constructor() { 12 | this.created = null; 13 | this.updated = null; 14 | } 15 | 16 | async new(data: UpdateScheduleControllerInterface): Promise { 17 | log.info( 18 | 'UpdateScheduleController: Creating a new environment update schedule.' 19 | ); 20 | this.created = await prismaClient.updateSchedule.create({ 21 | data, 22 | }); 23 | 24 | return this.created; 25 | } 26 | 27 | async update( 28 | data: UpdateScheduleControllerInterface 29 | ): Promise { 30 | log.info( 31 | 'UpdateScheduleController: Updating an environment update schedule.' 32 | ); 33 | 34 | this.updated = await prismaClient.updateSchedule.update({ 35 | where: { 36 | environmentId: data.environmentId, 37 | }, 38 | data, 39 | }); 40 | 41 | return this.updated; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/database/migrationHandler.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'electron'; 2 | import log from 'electron-log'; 3 | import * as fs from 'fs'; 4 | import path from 'path'; 5 | import runPrismaCommand from '../utils/runPrismaCommand'; 6 | import { 7 | dbName, 8 | dbPath, 9 | dbUrl, 10 | extraResourcesPath, 11 | isDevelopment, 12 | latestMigration, 13 | legacyDbName, 14 | } from '../utils/globalConstants'; 15 | import prismaClient from './prismaContext'; 16 | import seedDb from './seedDb'; 17 | import { Migration } from '../interfaces/MigrationInterface'; 18 | import LogController from '../controllers/LogController'; 19 | 20 | export default async function runDbMigrations() { 21 | let needsMigration = false; 22 | let mustSeed = false; 23 | const fullDbPath = path.resolve(dbPath, dbName); 24 | 25 | log.info(`Checking database at ${fullDbPath}`); 26 | 27 | // checks if the legacy db exists (app.db), which was the name used until v0.2.1 28 | const legacyDbExists = fs.existsSync(path.resolve(dbPath, legacyDbName)); 29 | if (legacyDbExists) { 30 | log.info( 31 | `Legacy database detected at ${path.resolve(dbPath, legacyDbName)}.` 32 | ); 33 | log.info(`Database will be renamed to ${dbName}.`); 34 | 35 | fs.renameSync(path.resolve(dbPath, legacyDbName), fullDbPath); 36 | } 37 | 38 | const dbExists = fs.existsSync(fullDbPath); 39 | 40 | if (!dbExists) { 41 | log.info('Database does not exists. Migration and seeding is needed.'); 42 | needsMigration = true; 43 | mustSeed = true; 44 | // since prisma has trouble if the database file does not exist, touches an empty file 45 | log.info('Touching database file.'); 46 | fs.closeSync(fs.openSync(fullDbPath, 'w')); 47 | } else { 48 | log.info('Database exists. Verifying the latest migration'); 49 | log.info(`Latest generated migration is: ${latestMigration}`); 50 | try { 51 | const latest: Migration[] = 52 | await prismaClient.$queryRaw`select * from _prisma_migrations order by finished_at`; 53 | log.info( 54 | `Latest migration on the database: ${ 55 | latest[latest.length - 1]?.migration_name 56 | }` 57 | ); 58 | needsMigration = 59 | latest[latest.length - 1]?.migration_name !== latestMigration; 60 | } catch (e) { 61 | log.info( 62 | 'Latest migration could not be found, migration is needed. Error details:' 63 | ); 64 | log.error(e); 65 | needsMigration = true; 66 | } 67 | } 68 | 69 | if (needsMigration) { 70 | try { 71 | const schemaPath = isDevelopment 72 | ? path.resolve(extraResourcesPath, 'prisma', 'schema.prisma') 73 | : path.resolve(app.getAppPath(), '..', 'prisma', 'schema.prisma'); 74 | log.info( 75 | `Database needs a migration. Running prisma migrate with schema path ${schemaPath}` 76 | ); 77 | 78 | await runPrismaCommand({ 79 | command: ['migrate', 'deploy', '--schema', schemaPath], 80 | dbUrl, 81 | }); 82 | 83 | log.info('Migration done.'); 84 | 85 | if (mustSeed) { 86 | await seedDb(prismaClient); 87 | 88 | await new LogController().writeLog({ 89 | type: 'info', 90 | message: 'Initial database seed executed with default values', 91 | }); 92 | } 93 | 94 | log.info('Creating a database migration notification'); 95 | await prismaClient.notification.create({ 96 | data: { 97 | type: 'info', 98 | title: 'Base de dados migrada', 99 | body: 'O banco de dados foi migrado devido à atualização de versão do aplicativo.', 100 | }, 101 | }); 102 | 103 | await new LogController().writeLog({ 104 | type: 'info', 105 | message: 'Database migration executed', 106 | }); 107 | } catch (e) { 108 | log.error('Migration executed with error.'); 109 | log.error(e); 110 | 111 | await new LogController().writeLog({ 112 | type: 'error', 113 | message: `Database migration executed with error: ${e}`, 114 | }); 115 | process.exit(1); 116 | } 117 | } else { 118 | log.info('Does not need migration'); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/main/database/prismaContext.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '../generated/client'; 2 | import { qePath, dbUrl } from '../utils/globalConstants'; 3 | 4 | const prismaClient = new PrismaClient({ 5 | log: ['info', 'warn', 'error'], 6 | datasources: { 7 | db: { 8 | url: dbUrl, 9 | }, 10 | }, 11 | // see https://github.com/prisma/prisma/discussions/5200 12 | // @ts-expect-error internal prop 13 | __internal: { 14 | engine: { 15 | binaryPath: qePath, 16 | }, 17 | }, 18 | }); 19 | 20 | export default prismaClient; 21 | -------------------------------------------------------------------------------- /src/main/database/seedDb.ts: -------------------------------------------------------------------------------- 1 | import log from 'electron-log'; 2 | import { PrismaClient } from '../generated/client'; 3 | 4 | export default async function seedDb(prisma: PrismaClient) { 5 | log.info('Seeding the database with default values'); 6 | 7 | // app settings 8 | await prisma.appSetting.create({ 9 | data: { 10 | settingId: 'FRONT_END_THEME', 11 | value: 'WHITE', 12 | group: 'SYSTEM', 13 | }, 14 | }); 15 | await prisma.appSetting.create({ 16 | data: { 17 | settingId: 'APP_LANGUAGE', 18 | value: 'pt', 19 | group: 'SYSTEM', 20 | }, 21 | }); 22 | await prisma.appSetting.create({ 23 | data: { 24 | settingId: 'APP_RELAY_MODE', 25 | value: 'MASTER', // or RELAY 26 | group: 'SYSTEM', 27 | }, 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /src/main/interfaces/GitHubReleaseInterface.ts: -------------------------------------------------------------------------------- 1 | export interface ReleaseAuthor { 2 | login: string; 3 | id: number; 4 | node_id: string; 5 | avatar_url: string; 6 | gravatar_id: string; 7 | url: string; 8 | html_url: string; 9 | followers_url: string; 10 | following_url: string; 11 | gists_url: string; 12 | starred_url: string; 13 | subscriptions_url: string; 14 | organizations_url: string; 15 | repos_url: string; 16 | events_url: string; 17 | received_events_url: string; 18 | type: string; 19 | site_admin: boolean; 20 | } 21 | 22 | export interface ReleaseUploader { 23 | login: string; 24 | id: number; 25 | node_id: string; 26 | avatar_url: string; 27 | gravatar_id: string; 28 | url: string; 29 | html_url: string; 30 | followers_url: string; 31 | following_url: string; 32 | gists_url: string; 33 | starred_url: string; 34 | subscriptions_url: string; 35 | organizations_url: string; 36 | repos_url: string; 37 | events_url: string; 38 | received_events_url: string; 39 | type: string; 40 | site_admin: boolean; 41 | } 42 | 43 | export interface ReleaseAsset { 44 | url: string; 45 | id: number; 46 | node_id: string; 47 | name: string; 48 | label?: string; 49 | uploader: ReleaseUploader; 50 | content_type: string; 51 | state: string; 52 | size: number; 53 | download_count: number; 54 | created_at: Date; 55 | updated_at: Date; 56 | browser_download_url: string; 57 | } 58 | 59 | export default interface GitHubReleaseInterface { 60 | url: string; 61 | assets_url: string; 62 | upload_url: string; 63 | html_url: string; 64 | id: number; 65 | author: ReleaseAuthor; 66 | node_id: string; 67 | tag_name: string; 68 | target_commitish: string; 69 | name: string; 70 | draft: boolean; 71 | prerelease: boolean; 72 | created_at: Date; 73 | published_at: Date; 74 | assets: ReleaseAsset[]; 75 | tarball_url: string; 76 | zipball_url: string; 77 | body: string; 78 | mentions_count: number; 79 | } 80 | -------------------------------------------------------------------------------- /src/main/interfaces/MigrationInterface.ts: -------------------------------------------------------------------------------- 1 | export interface Migration { 2 | id: string; 3 | checksum: string; 4 | finished_at: string; 5 | migration_name: string; 6 | logs: string; 7 | rolled_back_at: string; 8 | started_at: string; 9 | applied_steps_count: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/preload.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge } from 'electron'; 2 | 3 | contextBridge.exposeInMainWorld('electron', { 4 | // functions to be exposed (not yet used) 5 | }); 6 | -------------------------------------------------------------------------------- /src/main/services/getEnvironmentRelease.ts: -------------------------------------------------------------------------------- 1 | import log from 'electron-log'; 2 | import AuthObject from '../../common/interfaces/AuthObject'; 3 | import FluigAPIClient from '../../common/classes/FluigAPIClient'; 4 | import { FluigVersionApiInterface } from '../../common/interfaces/FluigVersionApiInterface'; 5 | 6 | export default async function getEnvironmentRelease( 7 | auth: AuthObject, 8 | domainUrl: string 9 | ): Promise { 10 | try { 11 | if (!auth || !domainUrl) { 12 | throw new Error('Required parameters were not provided'); 13 | } 14 | 15 | const endpoint = '/api/public/wcm/version/v2'; 16 | log.info( 17 | `getEnvironmentRelease: Recovering environment release from ${domainUrl}${endpoint}` 18 | ); 19 | 20 | let version = null; 21 | 22 | const fluigClient = new FluigAPIClient({ 23 | oAuthKeys: auth, 24 | requestData: { 25 | method: 'GET', 26 | url: domainUrl + endpoint, 27 | }, 28 | }); 29 | 30 | await fluigClient.get(); 31 | 32 | if (fluigClient.httpStatus === 200) { 33 | version = fluigClient.httpResponse; 34 | } else { 35 | log.error( 36 | `getEnvironmentRelease: An error occurred while checking permission: ${fluigClient.errorStack}` 37 | ); 38 | } 39 | 40 | return version; 41 | } catch (error) { 42 | log.error(`getEnvironmentRelease: An error occurred: ${error}`); 43 | return null; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/services/validateOAuthPermission.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-await-in-loop */ 2 | import log from 'electron-log'; 3 | import AuthObject from '../../common/interfaces/AuthObject'; 4 | import FluigAPIClient from '../../common/classes/FluigAPIClient'; 5 | 6 | /** 7 | * Validates if the oAuth user has the necessary permissions to collect data from the Fluig server 8 | * @since 0.2.2 9 | */ 10 | export default async function validateOAuthPermission( 11 | auth: AuthObject, 12 | domainUrl: string 13 | ) { 14 | const results = []; 15 | 16 | try { 17 | if (!auth || !domainUrl) { 18 | throw new Error('Required parameters were not provided'); 19 | } 20 | 21 | log.info(`validateOAuthPermission: Validating oAuth user permissions`); 22 | const endpoints = [ 23 | '/api/servlet/ping', 24 | '/monitoring/api/v1/statistics/report', 25 | '/monitoring/api/v1/monitors/report', 26 | '/license/api/v1/licenses', 27 | '/api/public/wcm/version/v2', 28 | ]; 29 | 30 | let fluigClient = null; 31 | 32 | for (let i = 0; i < endpoints.length; i += 1) { 33 | const endpoint = endpoints[i]; 34 | 35 | fluigClient = new FluigAPIClient({ 36 | oAuthKeys: auth, 37 | requestData: { 38 | method: 'GET', 39 | url: domainUrl + endpoint, 40 | }, 41 | }); 42 | 43 | await fluigClient.get(); 44 | 45 | if (fluigClient.httpStatus === 200) { 46 | log.info('validateOAuthPermission: Permission is valid'); 47 | } else if ( 48 | fluigClient.httpStatus === 401 || 49 | fluigClient.httpStatus === 403 50 | ) { 51 | log.warn('validateOAuthPermission: Permission is invalid'); 52 | } else { 53 | log.error( 54 | `validateOAuthPermission: An error occurred while checking permission: ${fluigClient.errorStack}` 55 | ); 56 | } 57 | 58 | results.push({ 59 | endpoint, 60 | httpStatus: fluigClient.httpStatus, 61 | }); 62 | } 63 | } catch (error) { 64 | log.error(`validateOAuthPermission: An error occurred: ${error}`); 65 | } 66 | 67 | return results; 68 | } 69 | -------------------------------------------------------------------------------- /src/main/utils/frequencyToMs.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts a frequency value to a number in milliseconds 3 | * 4 | * @example 5 | * frequencyToMs('15m') => 900000 6 | */ 7 | export default function frequencyToMs(dbFrequency: string): number { 8 | const modifierIndex = dbFrequency.search(/[\D]/); 9 | 10 | const modifier = dbFrequency.charAt(modifierIndex); 11 | 12 | const frequency = Number(dbFrequency.substring(0, modifierIndex)); 13 | 14 | if (modifier === 's') { 15 | return frequency * 1000; 16 | } 17 | if (modifier === 'm') { 18 | return frequency * 60 * 1000; 19 | } 20 | if (modifier === 'h') { 21 | return frequency * 60 * 60 * 1000; 22 | } 23 | 24 | return 0; 25 | } 26 | -------------------------------------------------------------------------------- /src/main/utils/fsUtils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { app } from 'electron'; 3 | import path from 'path'; 4 | import * as fs from 'fs'; 5 | import log from 'electron-log'; 6 | 7 | /** 8 | * @function getAppDataFolder 9 | * @description gets the app folder path under the "appData" folder, and creates it if not exists 10 | * @returns {string} app folder path 11 | * @since 0.1.0 12 | */ 13 | export default function getAppDataFolder(): string { 14 | const folderPath = path.resolve(app.getPath('appData'), 'fluig-monitor'); 15 | 16 | // checks if the app folder exists, and if not, creates it. 17 | if (!fs.existsSync(folderPath)) { 18 | fs.mkdirSync(folderPath); 19 | log.info(`Folder ${folderPath} does not exist and will be created.`); 20 | } 21 | 22 | return folderPath; 23 | } 24 | -------------------------------------------------------------------------------- /src/main/utils/getAssetPath.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'electron'; 2 | import path from 'path'; 3 | 4 | const RESOURCES_PATH = app.isPackaged 5 | ? path.join(process.resourcesPath, 'assets') 6 | : path.join(__dirname, '../../../assets'); 7 | 8 | const getAssetPath = (...paths: string[]): string => { 9 | return path.join(RESOURCES_PATH, ...paths); 10 | }; 11 | 12 | export default getAssetPath; 13 | -------------------------------------------------------------------------------- /src/main/utils/globalConstants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This constants file should be used only on the main process, since it creates an error on the renderer 3 | * where the "app" gets undefined on the renderer 4 | */ 5 | 6 | /* eslint-disable global-require */ 7 | import path from 'path'; 8 | import { app } from 'electron'; 9 | import getAppDataFolder from './fsUtils'; 10 | 11 | export const isDevelopment = 12 | process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true'; 13 | 14 | if (isDevelopment) { 15 | require('dotenv').config(); 16 | } 17 | 18 | export const logStringFormat = 19 | '{y}-{m}-{d} {h}:{i}:{s}.{ms} [{level}] ({processType}) {text}'; 20 | 21 | export const scrapeSyncInterval = 900000; // 15 minutes 22 | export const scrapeSyncIntervalCron = '* */15 * * * *'; 23 | export const pingInterval = 15000; // 15 seconds 24 | export const pingIntervalCron = '*/15 * * * * *'; 25 | 26 | export const legacyDbName = 'app.db'; 27 | export const dbName = 'fluig-monitor.db'; 28 | export const dbPath = isDevelopment 29 | ? path.resolve(__dirname, '../../../', 'prisma') 30 | : path.resolve(getAppDataFolder()); 31 | export const dbUrl = 32 | (isDevelopment 33 | ? process.env.DATABASE_URL 34 | : `file:${path.resolve(dbPath, dbName)}`) || ''; 35 | 36 | // Must be updated every time a migration is created 37 | export const latestMigration = '20221205230300_create_resource_type_field'; 38 | 39 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 40 | export const platformToExecutables: any = { 41 | win32: { 42 | migrationEngine: 43 | 'node_modules/@prisma/engines/migration-engine-windows.exe', 44 | queryEngine: 'node_modules/@prisma/engines/query_engine-windows.dll.node', 45 | }, 46 | linux: { 47 | migrationEngine: 48 | 'node_modules/@prisma/engines/migration-engine-debian-openssl-3.0.x', 49 | queryEngine: 50 | 'node_modules/@prisma/engines/libquery_engine-debian-openssl-3.0.x.so.node', 51 | }, 52 | darwin: { 53 | migrationEngine: 'node_modules/@prisma/engines/migration-engine-darwin', 54 | queryEngine: 55 | 'node_modules/@prisma/engines/libquery_engine-darwin.dylib.node', 56 | }, 57 | darwinArm64: { 58 | migrationEngine: 59 | 'node_modules/@prisma/engines/migration-engine-darwin-arm64', 60 | queryEngine: 61 | 'node_modules/@prisma/engines/libquery_engine-darwin-arm64.dylib.node', 62 | }, 63 | }; 64 | 65 | export const extraResourcesPath = isDevelopment 66 | ? path.resolve(__dirname, '../../../') 67 | : app.getAppPath().replace('app.asar', ''); 68 | 69 | function getPlatformName(): string { 70 | const isDarwin = process.platform === 'darwin'; 71 | if (isDarwin && process.arch === 'arm64') { 72 | return `${process.platform}Arm64`; 73 | } 74 | 75 | return process.platform; 76 | } 77 | 78 | const platformName = getPlatformName(); 79 | 80 | export const mePath = path.join( 81 | extraResourcesPath, 82 | platformToExecutables[platformName].migrationEngine 83 | ); 84 | export const qePath = path.join( 85 | extraResourcesPath, 86 | platformToExecutables[platformName].queryEngine 87 | ); 88 | -------------------------------------------------------------------------------- /src/main/utils/logRotation.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import path from 'path'; 3 | import * as fs from 'fs'; 4 | import log from 'electron-log'; 5 | import compressing from 'compressing'; 6 | import { isDevelopment } from './globalConstants'; 7 | import getAppDataFolder from './fsUtils'; 8 | 9 | /** 10 | * Archives the app log file on a daily basis with a custom name, preventing it from being too large. 11 | * @since 0.1.0 12 | */ 13 | export default async function rotateLogFile(): Promise { 14 | try { 15 | const today = new Date(); 16 | const filePath = path.resolve( 17 | getAppDataFolder(), 18 | 'logs', 19 | isDevelopment ? 'fluig-monitor.dev.log' : 'fluig-monitor.log' 20 | ); 21 | const yesterday = new Date().setDate(today.getDate() - 1); 22 | const yesterdayFileFormat = new Date(yesterday) 23 | .toLocaleDateString('pt') 24 | .split('/') 25 | .reverse() 26 | .join('-'); 27 | let logContent = null; 28 | 29 | // checks if the log file exists 30 | if (!fs.existsSync(filePath)) { 31 | return; 32 | } 33 | 34 | // if the current log file was last modified on a date previous to today and the rotated log file does not exits 35 | if ( 36 | fs.statSync(filePath).mtime.getDate() !== today.getDate() && 37 | !fs.existsSync(filePath.replace('.log', `_${yesterdayFileFormat}.log`)) 38 | ) { 39 | const archiveFilePath = filePath.replace( 40 | '.log', 41 | `_${yesterdayFileFormat}.log` 42 | ); 43 | 44 | logContent = fs.readFileSync(filePath, 'utf-8'); 45 | 46 | fs.writeFileSync(archiveFilePath, logContent); 47 | 48 | fs.writeFileSync(filePath, ''); 49 | 50 | await compressing.zip.compressFile( 51 | archiveFilePath, 52 | archiveFilePath.replace('.log', '.zip') 53 | ); 54 | 55 | fs.rmSync(archiveFilePath); 56 | 57 | log.info( 58 | `Previous log file has been archived as ${archiveFilePath} due to file rotation` 59 | ); 60 | } 61 | } catch (e: any) { 62 | log.error('Could not rotate log file: '); 63 | log.error(e.stack); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/utils/logSystemConfigs.ts: -------------------------------------------------------------------------------- 1 | import os from 'node:os'; 2 | import log from 'electron-log'; 3 | import formatBytes from '../../common/utils/formatBytes'; 4 | 5 | export default function logSystemConfigs() { 6 | const cpus = os.cpus(); 7 | log.info('============ System Configuration ============'); 8 | log.info(`CPU: ${cpus.length}x ${cpus[0].model}`); 9 | log.info(`Total RAM: ${os.totalmem()} bytes (${formatBytes(os.totalmem())})`); 10 | log.info(`Free RAM: ${os.freemem()} bytes (${formatBytes(os.freemem())})`); 11 | log.info(`System Uptime: ${os.uptime()}s`); 12 | log.info(`OS Type: ${os.type()}`); 13 | log.info(`Platform: ${os.platform()}`); 14 | log.info(`OS Release: ${os.release()}`); 15 | log.info(`Arch: ${os.arch()}`); 16 | log.info('=============================================='); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/utils/resolveHtmlPath.ts: -------------------------------------------------------------------------------- 1 | /* eslint import/prefer-default-export: off, import/no-mutable-exports: off */ 2 | import { URL } from 'url'; 3 | import path from 'path'; 4 | 5 | export let resolveHtmlPath: (htmlFileName: string) => string; 6 | 7 | if (process.env.NODE_ENV === 'development') { 8 | const port = process.env.PORT || 1212; 9 | resolveHtmlPath = (htmlFileName: string) => { 10 | const url = new URL(`http://localhost:${port}`); 11 | url.pathname = htmlFileName; 12 | return url.href; 13 | }; 14 | } else { 15 | resolveHtmlPath = (htmlFileName: string) => { 16 | return `file://${path.resolve(__dirname, '../renderer/', htmlFileName)}`; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/main/utils/runPrismaCommand.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names */ 2 | import path from 'path'; 3 | import log from 'electron-log'; 4 | import { fork } from 'child_process'; 5 | import { mePath, qePath } from './globalConstants'; 6 | 7 | export default async function runPrismaCommand({ 8 | command, 9 | dbUrl, 10 | }: { 11 | command: string[]; 12 | dbUrl: string; 13 | }): Promise { 14 | log.info('Migration engine path', mePath); 15 | log.info('Query engine path', qePath); 16 | 17 | // Currently we don't have any direct method to invoke prisma migration programatically. 18 | // As a workaround, we spawn migration script as a child process and wait for its completion. 19 | // Please also refer to the following GitHub issue: https://github.com/prisma/prisma/issues/4703 20 | try { 21 | const exitCode = await new Promise((resolve /* , reject */) => { 22 | const prismaPath = path.resolve( 23 | __dirname, 24 | '..', 25 | '..', 26 | '..', 27 | 'node_modules/prisma/build/index.js' 28 | ); 29 | log.info('Prisma path', prismaPath); 30 | 31 | const child = fork(prismaPath, command, { 32 | env: { 33 | ...process.env, 34 | DATABASE_URL: dbUrl, 35 | PRISMA_MIGRATION_ENGINE_BINARY: mePath, 36 | PRISMA_QUERY_ENGINE_LIBRARY: qePath, 37 | }, 38 | stdio: 'pipe', 39 | }); 40 | 41 | child.on('message', (msg) => { 42 | log.info('Message from child:', msg); 43 | }); 44 | 45 | child.on('error', (err) => { 46 | log.error('Child process got an error:', err); 47 | }); 48 | 49 | child.on('close', (code) => { 50 | log.info('Child process is being closed. (Exit code', code, ')'); 51 | resolve(code); 52 | }); 53 | 54 | child.stdout?.on('data', (data) => { 55 | log.info('prisma info: ', data.toString()); 56 | }); 57 | 58 | child.stderr?.on('data', (data) => { 59 | log.error('prisma error: ', data.toString()); 60 | }); 61 | }); 62 | 63 | if (exitCode !== 0) { 64 | throw Error(`command ${command} failed with exit code ${exitCode}`); 65 | } 66 | return exitCode; 67 | } catch (e) { 68 | log.error(e); 69 | throw e; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/utils/trayBuilder.ts: -------------------------------------------------------------------------------- 1 | import { app, Menu, shell, Tray } from 'electron'; 2 | import log from 'electron-log'; 3 | import path from 'path'; 4 | import i18n from '../../common/i18n/i18n'; 5 | import getAssetPath from './getAssetPath'; 6 | import { version } from '../../../package.json'; 7 | 8 | export default function trayBuilder( 9 | instance: Tray | null, 10 | reopenFunction: () => void 11 | ): Tray { 12 | if (instance !== null) instance.destroy(); 13 | 14 | const newInstance = new Tray(path.join(getAssetPath(), 'icon.ico')); 15 | newInstance.setToolTip(i18n.t('menu.systemTray.running')); 16 | newInstance.on('click', reopenFunction); 17 | newInstance.setContextMenu( 18 | Menu.buildFromTemplate([ 19 | { 20 | type: 'normal', 21 | label: `Fluig Monitor - v${version}`, 22 | enabled: false, 23 | }, 24 | { type: 'separator' }, 25 | { 26 | type: 'normal', 27 | label: i18n.t('menu.systemTray.open'), 28 | click: reopenFunction, 29 | }, 30 | { 31 | type: 'normal', 32 | label: i18n.t('menu.systemTray.reportABug'), 33 | click: () => 34 | shell.openExternal( 35 | 'https://github.com/luizf-lf/fluig-monitor/issues/new/choose' 36 | ), 37 | }, 38 | { 39 | type: 'normal', 40 | label: i18n.t('menu.systemTray.dropBombs'), 41 | enabled: false, 42 | }, 43 | { 44 | type: 'normal', 45 | label: i18n.t('menu.systemTray.quit'), 46 | click: () => { 47 | log.info( 48 | 'App will be closed since the system tray option has been clicked.' 49 | ); 50 | app.quit(); 51 | }, 52 | }, 53 | ]) 54 | ); 55 | 56 | return newInstance; 57 | } 58 | -------------------------------------------------------------------------------- /src/renderer/App.tsx: -------------------------------------------------------------------------------- 1 | import { Routes, Route } from 'react-router-dom'; 2 | import { AnimatePresence } from 'framer-motion'; 3 | 4 | // views / components 5 | import EnvironmentView from './pages/EnvironmentView'; 6 | import CreateEnvironmentView from './pages/CreateEnvironmentView'; 7 | import Navbar from './components/container/Navbar/Navbar'; 8 | import HomeEnvironmentListView from './pages/HomeEnvironmentListView'; 9 | import AppSettingsView from './pages/AppSettingsView'; 10 | 11 | // assets 12 | import './assets/styles/global.scss'; 13 | import './assets/styles/utilities.scss'; 14 | 15 | // contexts 16 | import { EnvironmentListContextProvider } from './contexts/EnvironmentListContext'; 17 | import { NotificationsContextProvider } from './contexts/NotificationsContext'; 18 | import { ThemeContextProvider } from './contexts/ThemeContext'; 19 | 20 | export default function App() { 21 | // the useLocation hook is used to render a specific component per route 22 | // const location = useLocation(); 23 | 24 | return ( 25 | 26 | 27 | 28 |
29 | 30 |
31 | 32 | {/* */} 33 | 34 | } /> 35 | } 38 | /> 39 | } 42 | /> 43 | } /> 44 | 45 | 46 |
47 |
48 |
49 |
50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /src/renderer/assets/img/banner_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizf-lf/fluig-monitor/23df4d7feb24e3de7a769f917bad9ee8d7892e71/src/renderer/assets/img/banner_logo.png -------------------------------------------------------------------------------- /src/renderer/assets/img/database-logos/database.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizf-lf/fluig-monitor/23df4d7feb24e3de7a769f917bad9ee8d7892e71/src/renderer/assets/img/database-logos/database.png -------------------------------------------------------------------------------- /src/renderer/assets/img/database-logos/mysql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizf-lf/fluig-monitor/23df4d7feb24e3de7a769f917bad9ee8d7892e71/src/renderer/assets/img/database-logos/mysql.png -------------------------------------------------------------------------------- /src/renderer/assets/img/database-logos/oracle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizf-lf/fluig-monitor/23df4d7feb24e3de7a769f917bad9ee8d7892e71/src/renderer/assets/img/database-logos/oracle.png -------------------------------------------------------------------------------- /src/renderer/assets/img/database-logos/sql-server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizf-lf/fluig-monitor/23df4d7feb24e3de7a769f917bad9ee8d7892e71/src/renderer/assets/img/database-logos/sql-server.png -------------------------------------------------------------------------------- /src/renderer/assets/img/defaultServerLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizf-lf/fluig-monitor/23df4d7feb24e3de7a769f917bad9ee8d7892e71/src/renderer/assets/img/defaultServerLogo.png -------------------------------------------------------------------------------- /src/renderer/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizf-lf/fluig-monitor/23df4d7feb24e3de7a769f917bad9ee8d7892e71/src/renderer/assets/img/logo.png -------------------------------------------------------------------------------- /src/renderer/assets/img/theme-preview-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizf-lf/fluig-monitor/23df4d7feb24e3de7a769f917bad9ee8d7892e71/src/renderer/assets/img/theme-preview-dark.png -------------------------------------------------------------------------------- /src/renderer/assets/img/theme-preview-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizf-lf/fluig-monitor/23df4d7feb24e3de7a769f917bad9ee8d7892e71/src/renderer/assets/img/theme-preview-white.png -------------------------------------------------------------------------------- /src/renderer/assets/styles/components/CreateEnvironmentButton.scss: -------------------------------------------------------------------------------- 1 | .createEnvironmentButton { 2 | border: 2px dashed var(--border-light); 3 | color: var(--border-light); 4 | display: flex; 5 | text-decoration: none; 6 | 7 | border-radius: 0.75rem; 8 | font-size: 1.2rem; 9 | padding: 0.75rem 1rem; 10 | align-items: center; 11 | justify-content: center; 12 | transition: all var(--transition); 13 | 14 | &:hover { 15 | border: 2px dashed var(--border); 16 | color: var(--border); 17 | } 18 | 19 | &:active { 20 | transform: scale(90%); 21 | } 22 | 23 | &.is-expanded { 24 | border: 2px dashed var(--border); 25 | color: var(--border); 26 | height: inherit; 27 | min-height: 100%; 28 | padding: 2rem; 29 | 30 | margin-right: 2rem; 31 | 32 | &:hover { 33 | box-shadow: var(--card-shadow); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/renderer/assets/styles/components/EnvironmentAvailabilityPanel.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | &.environment-status-card { 3 | display: flex; 4 | flex-direction: column; 5 | gap: 1rem; 6 | 7 | max-width: 28rem; 8 | 9 | .header { 10 | display: flex; 11 | align-items: center; 12 | justify-content: flex-start; 13 | 14 | gap: 0.5rem; 15 | } 16 | 17 | .body { 18 | display: flex; 19 | justify-content: space-between; 20 | gap: 0.5rem; 21 | 22 | .status-message { 23 | h3 { 24 | margin-bottom: 0.25rem; 25 | font-size: 2rem; 26 | font-weight: bold; 27 | } 28 | 29 | span { 30 | color: var(--font-soft); 31 | font-size: 0.9rem; 32 | } 33 | } 34 | 35 | .status-icon { 36 | color: #fff; 37 | background-color: var(--green); 38 | padding: 1rem; 39 | border-radius: 100%; 40 | display: flex; 41 | align-items: center; 42 | justify-content: center; 43 | margin-right: 1rem; 44 | 45 | min-width: fit-content; 46 | 47 | &.breathe { 48 | animation: breathing 5s cubic-bezier(0.1, 0.8, 0.4, 1) infinite; 49 | } 50 | 51 | &.has-warning { 52 | background-color: var(--yellow); 53 | } 54 | 55 | &.has-danger { 56 | background-color: var(--red); 57 | } 58 | 59 | svg { 60 | font-size: 3rem; 61 | } 62 | } 63 | } 64 | 65 | .footer { 66 | span { 67 | color: var(--font-soft); 68 | font-size: 0.8rem; 69 | } 70 | } 71 | } 72 | 73 | &.system-resource-card { 74 | height: 100%; 75 | min-width: 13rem; 76 | 77 | display: flex; 78 | justify-content: space-between; 79 | flex-direction: column; 80 | 81 | .header { 82 | display: flex; 83 | align-items: center; 84 | justify-content: flex-start; 85 | 86 | gap: 0.5rem; 87 | } 88 | 89 | .body { 90 | display: flex; 91 | flex-direction: column; 92 | justify-content: center; 93 | 94 | margin-top: 0.5rem; 95 | 96 | h3 { 97 | margin: 0; 98 | font-size: 1.75rem; 99 | font-weight: bold; 100 | } 101 | } 102 | 103 | .footer { 104 | .database-traffic-container { 105 | display: flex; 106 | align-items: center; 107 | justify-content: space-between; 108 | gap: 0.5rem; 109 | 110 | .received, 111 | .sent { 112 | display: flex; 113 | align-items: center; 114 | justify-content: space-between; 115 | 116 | font-size: 0.8rem; 117 | } 118 | } 119 | } 120 | } 121 | } 122 | 123 | @keyframes breathing { 124 | 0% { 125 | box-shadow: 0 0 0 0px rgba(16, 185, 129, 0.75); 126 | } 127 | 75% { 128 | box-shadow: 0 0 5px 25px rgba(16, 185, 129, 0); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/renderer/assets/styles/components/EnvironmentListItem.scss: -------------------------------------------------------------------------------- 1 | .environment-item-container { 2 | background-color: var(--background); 3 | color: var(--font-secondary); 4 | font-weight: bold; 5 | 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | 10 | padding: 1rem; 11 | border-radius: 0.75rem; 12 | 13 | text-decoration: none; 14 | transition: transform var(--transition); 15 | background-color: transparent; 16 | border: 2px solid var(--border-light); 17 | font-weight: 500; 18 | 19 | position: relative; 20 | 21 | .environment-indicators { 22 | position: absolute; 23 | 24 | bottom: 0.5rem; 25 | left: 0.5rem; 26 | 27 | display: flex; 28 | 29 | .online-indicator { 30 | height: 0.35rem; 31 | width: 0.35rem; 32 | border-radius: 100%; 33 | background-color: var(--green); 34 | 35 | margin-right: 0.25rem; 36 | 37 | &.is-offline { 38 | background-color: var(--red); 39 | } 40 | } 41 | 42 | .kind-indicator { 43 | height: 0.35rem; 44 | width: 0.35rem; 45 | border-radius: 100%; 46 | 47 | &.is-prod { 48 | background-color: var(--green); 49 | } 50 | 51 | &.is-hml { 52 | background-color: var(--yellow); 53 | } 54 | 55 | &.is-dev { 56 | background-color: var(--purple); 57 | } 58 | } 59 | } 60 | 61 | &:active { 62 | transform: scale(95%); 63 | } 64 | 65 | &.is-expanded { 66 | padding: 0.35rem; 67 | 68 | .initials { 69 | background-color: var(--purple); 70 | color: #fff; 71 | padding: 0.65rem; 72 | border-radius: 0.5rem; 73 | margin-right: 0.5rem; 74 | } 75 | 76 | .data { 77 | display: flex; 78 | flex-direction: column; 79 | 80 | .bottom { 81 | display: flex; 82 | justify-content: space-between; 83 | align-items: baseline; 84 | 85 | .statusIndicator { 86 | display: flex; 87 | align-items: center; 88 | 89 | margin-right: 0.5rem; 90 | 91 | .dot { 92 | height: 0.35rem; 93 | width: 0.35rem; 94 | border-radius: 100%; 95 | background-color: var(--green); 96 | 97 | margin-right: 0.25rem; 98 | } 99 | 100 | .description { 101 | text-transform: uppercase; 102 | font-size: 0.5rem; 103 | color: var(--green); 104 | 105 | font-weight: 500; 106 | } 107 | 108 | &.is-offline { 109 | .dot { 110 | background-color: var(--red); 111 | } 112 | 113 | .description { 114 | color: var(--red); 115 | } 116 | } 117 | } 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/renderer/assets/styles/components/EnvironmentServerInfo.scss: -------------------------------------------------------------------------------- 1 | #environment-server-info { 2 | .widget-card { 3 | .image-container { 4 | text-align: center; 5 | 6 | margin-bottom: 1rem; 7 | 8 | img { 9 | max-width: 14rem; 10 | } 11 | } 12 | 13 | #server-specs { 14 | display: flex; 15 | justify-content: space-between; 16 | margin-bottom: 1rem; 17 | } 18 | 19 | .specs-item { 20 | display: flex; 21 | gap: 0.25rem; 22 | 23 | svg { 24 | font-size: 1.3rem; 25 | } 26 | 27 | .spec-description { 28 | display: flex; 29 | flex-direction: column; 30 | 31 | span { 32 | font-size: 0.6rem; 33 | 34 | &:nth-child(1) { 35 | color: var(--font-soft); 36 | } 37 | } 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/renderer/assets/styles/components/EnvironmentServices.scss: -------------------------------------------------------------------------------- 1 | #environment-services { 2 | .service-list { 3 | display: flex; 4 | flex-direction: column; 5 | 6 | .service-item { 7 | display: flex; 8 | justify-content: space-between; 9 | 10 | padding-bottom: 0.5rem; 11 | margin-bottom: 0.5rem; 12 | border-bottom: 2px solid var(--border-light); 13 | 14 | .service-name { 15 | font-size: 0.9rem; 16 | } 17 | 18 | .service-status { 19 | font-size: 0.6rem; 20 | text-transform: uppercase; 21 | 22 | .status-indicator { 23 | margin-left: 0.25rem; 24 | 25 | &::before { 26 | content: ''; 27 | 28 | height: 0.5rem; 29 | width: 0.5rem; 30 | border-radius: 100%; 31 | display: inline-block; 32 | background-color: #a7a7a7; 33 | } 34 | } 35 | 36 | &.is-operational { 37 | color: var(--green); 38 | .status-indicator { 39 | &::before { 40 | background-color: var(--green); 41 | } 42 | } 43 | } 44 | &.is-unused { 45 | color: var(--purple); 46 | .status-indicator { 47 | &::before { 48 | background-color: var(--purple); 49 | } 50 | } 51 | } 52 | &.is-failed { 53 | color: var(--red); 54 | .status-indicator { 55 | &::before { 56 | background-color: var(--red); 57 | } 58 | } 59 | } 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/renderer/assets/styles/components/FloatingNotification.scss: -------------------------------------------------------------------------------- 1 | .floatingNotificationContainer { 2 | width: 100%; 3 | box-shadow: var(--card-shadow); 4 | border-radius: var(--card-border-radius); 5 | 6 | display: flex; 7 | 8 | .iconContainer { 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | padding: 1rem; 13 | border-radius: 1rem 0 0 1rem; 14 | 15 | svg { 16 | font-size: 1.5rem; 17 | } 18 | } 19 | 20 | .messageContainer { 21 | padding: 1rem; 22 | display: flex; 23 | justify-content: flex-start; 24 | width: inherit; 25 | align-items: center; 26 | background-color: var(--card); 27 | border-radius: 0 1rem 1rem 0; 28 | 29 | max-height: 10rem; 30 | overflow-y: auto; 31 | 32 | color: var(--font-primary); 33 | } 34 | 35 | .closeButtonContainer { 36 | background-color: var(--background); 37 | border-radius: 0 1rem 1rem 0; 38 | padding: 1rem; 39 | } 40 | 41 | &.has-info { 42 | border: 2px solid var(--purple); 43 | .iconContainer { 44 | background-color: var(--light-purple); 45 | color: var(--purple); 46 | } 47 | } 48 | 49 | &.has-success { 50 | border: 2px solid var(--green); 51 | .iconContainer { 52 | background-color: var(--light-green); 53 | color: var(--green); 54 | } 55 | } 56 | 57 | &.has-warning { 58 | border: 2px solid var(--yellow); 59 | .iconContainer { 60 | background-color: var(--light-yellow); 61 | color: var(--yellow); 62 | } 63 | } 64 | 65 | &.has-error { 66 | border: 2px solid var(--red); 67 | .iconContainer { 68 | background-color: var(--light-red); 69 | color: var(--red); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/renderer/assets/styles/components/GraphTooltip.scss: -------------------------------------------------------------------------------- 1 | .custom-graph-tooltip { 2 | padding: 0.5rem; 3 | border-radius: 1rem; 4 | background-color: var(--background); 5 | box-shadow: var(--card-shadow); 6 | 7 | p { 8 | color: var(--font-primary); 9 | } 10 | 11 | .items-container { 12 | margin-top: 0.5rem; 13 | display: flex; 14 | flex-direction: column; 15 | 16 | span { 17 | font-size: 0.8rem; 18 | color: var(--font-secondary); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/renderer/assets/styles/components/Navbar.scss: -------------------------------------------------------------------------------- 1 | #mainNavbar { 2 | background: var(--card); 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | width: 100%; 7 | padding: 0.5rem 1rem; 8 | box-shadow: var(--card-shadow); 9 | 10 | display: flex; 11 | align-items: center; 12 | justify-content: space-between; 13 | 14 | z-index: 20; 15 | 16 | > div { 17 | display: flex; 18 | justify-content: center; 19 | align-items: center; 20 | } 21 | 22 | #logo-container { 23 | display: flex; 24 | align-items: center; 25 | 26 | padding-right: 1rem; 27 | margin-right: 1rem; 28 | border-right: 1px solid var(--border-light); 29 | 30 | img { 31 | height: 3rem; 32 | } 33 | 34 | .logoData { 35 | display: flex; 36 | flex-direction: column; 37 | justify-content: space-evenly; 38 | margin-left: 0.75rem; 39 | 40 | .title { 41 | font-family: Righteous, 'sans-serif'; 42 | color: #ff4d4d; 43 | font-size: 1.25rem; 44 | } 45 | 46 | .version { 47 | color: var(--font-soft); 48 | font-weight: 300; 49 | font-size: 0.75rem; 50 | } 51 | } 52 | } 53 | 54 | #environmentList { 55 | display: flex; 56 | gap: 0.5rem; 57 | 58 | padding-right: 1rem; 59 | border-right: 1px solid var(--border-light); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/renderer/assets/styles/components/ProgressBar.scss: -------------------------------------------------------------------------------- 1 | .pb-container { 2 | width: 100%; 3 | margin-top: 1rem; 4 | 5 | .values { 6 | display: flex; 7 | justify-content: space-between; 8 | font-size: 0.75rem; 9 | } 10 | 11 | .indicator { 12 | text-align: end; 13 | } 14 | 15 | .progress-bar { 16 | width: 100%; 17 | height: 0.7rem; 18 | 19 | border-radius: 0.5rem; 20 | 21 | background: var(--background); 22 | position: relative; 23 | 24 | .progress { 25 | height: 100%; 26 | border-radius: 0.5rem; 27 | 28 | &.progress-gradient { 29 | -webkit-mask: linear-gradient(#fff 0 0); 30 | mask: linear-gradient(#fff 0 0); 31 | 32 | &::before { 33 | content: ''; 34 | position: absolute; 35 | top: 0; 36 | left: 0; 37 | right: 0; 38 | bottom: 0; 39 | 40 | background: linear-gradient( 41 | 90deg, 42 | #34d399 0%, 43 | #fcd34d 50%, 44 | #ef4444 100% 45 | ); 46 | } 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/renderer/assets/styles/components/RightButtons.scss: -------------------------------------------------------------------------------- 1 | #rightButtons { 2 | display: flex; 3 | 4 | gap: 1rem; 5 | 6 | justify-content: center; 7 | align-items: center; 8 | 9 | svg { 10 | height: 1.25rem; 11 | width: 1.25rem; 12 | } 13 | 14 | .optionButton { 15 | padding: 0.75rem; 16 | background-color: var(--background); 17 | color: var(--font-secondary); 18 | 19 | display: flex; 20 | align-items: center; 21 | justify-content: center; 22 | 23 | border-radius: 100%; 24 | 25 | &:hover { 26 | background-color: var(--border-light); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/renderer/assets/styles/components/SmallTag.scss: -------------------------------------------------------------------------------- 1 | .small-tag { 2 | font-size: 0.5rem; 3 | font-weight: 500; 4 | 5 | &.is-production { 6 | color: var(--green); 7 | &.is-expanded { 8 | border: 2px solid var(--green); 9 | background-color: var(--light-green); 10 | } 11 | } 12 | 13 | &.is-homolog { 14 | color: var(--yellow); 15 | &.is-expanded { 16 | border: 2px solid var(--yellow); 17 | background-color: var(--light-yellow); 18 | } 19 | } 20 | 21 | &.is-dev { 22 | color: var(--purple); 23 | &.is-expanded { 24 | border: 2px solid var(--purple); 25 | background-color: var(--light-purple); 26 | } 27 | } 28 | 29 | &.is-expanded { 30 | padding: 0.15rem; 31 | border-radius: 0.25rem; 32 | font-size: 0.6rem; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/renderer/assets/styles/components/SpinnerLoader.scss: -------------------------------------------------------------------------------- 1 | .spinner-loader { 2 | width: 2.25rem; 3 | height: 2.25rem; 4 | border: 5px solid var(--border-light); 5 | border-bottom-color: var(--brand-medium); 6 | border-radius: 50%; 7 | display: inline-block; 8 | animation: rotation 1s linear infinite; 9 | } 10 | 11 | @keyframes rotation { 12 | 0% { 13 | transform: rotate(0deg); 14 | } 15 | 100% { 16 | transform: rotate(360deg); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/renderer/assets/styles/global.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;500;700&family=Righteous&display=swap'); 2 | 3 | :root { 4 | --font-primary: #1e293b; 5 | --font-secondary: #334155; 6 | --font-soft: #9ca3af; 7 | --background: #f1f5f9; 8 | --card: #ffffff; 9 | --border: #9ca3af; 10 | --border-light: #e5e7eb; 11 | 12 | --green: #10b981; 13 | --light-green: #dcfce7; 14 | --yellow: #f59e0b; 15 | --light-yellow: #fef9c3; 16 | --red: #ef4444; 17 | --light-red: #fee2e2; 18 | --purple: #6366f1; 19 | --light-purple: #e0e7ff; 20 | --blue: #60a5fa; 21 | --light-blue: #7dd3fc; 22 | 23 | --card-shadow: 0px 0px 40px rgba(0, 0, 0, 0.05); 24 | --card-shadow-sm: 0px 0px 10px rgba(0, 0, 0, 0.05); 25 | --card-border-radius-md: 0.75rem; 26 | --card-border-radius: 1rem; 27 | --transition: 300ms; 28 | 29 | --brand-light: #ff884d; 30 | --brand-medium: #ff4d4d; 31 | --brand-dark: #cc295f; 32 | --brand-gradient: linear-gradient( 33 | 45deg, 34 | var(--brand-light) 0%, 35 | var(--brand-medium) 50%, 36 | var(--brand-dark) 100% 37 | ); 38 | } 39 | 40 | .dark-theme { 41 | --font-primary: #fafafa; 42 | --font-secondary: #e4e4e4; 43 | --font-soft: #9ca3af; 44 | --background: #111827; 45 | --card: #1f2937; 46 | --border: #374151; 47 | --border-light: #374151; 48 | 49 | #logo-container { 50 | img { 51 | filter: brightness(0.9); 52 | } 53 | } 54 | } 55 | 56 | * { 57 | font-family: 'Inter'; 58 | font-size: 16px; 59 | font-weight: 500; 60 | 61 | margin: 0; 62 | padding: 0; 63 | border: 0; 64 | box-sizing: border-box; 65 | transition: background-color var(--transition); 66 | } 67 | 68 | body { 69 | background-color: var(--background); 70 | } 71 | 72 | button { 73 | background-color: transparent; 74 | cursor: pointer; 75 | 76 | &:focus-visible { 77 | outline: 3px solid var(--light-blue); 78 | } 79 | 80 | &:disabled { 81 | cursor: not-allowed; 82 | filter: opacity(0.75); 83 | } 84 | } 85 | 86 | a { 87 | text-decoration: none; 88 | 89 | &:visited { 90 | color: unset; 91 | } 92 | } 93 | 94 | h1, 95 | h2, 96 | h3, 97 | h4, 98 | h5, 99 | h6, 100 | span, 101 | p, 102 | div, 103 | label { 104 | color: var(--font-primary); 105 | } 106 | 107 | h1, 108 | h2, 109 | h3 { 110 | margin-bottom: 1.25rem; 111 | } 112 | 113 | h1 { 114 | font-size: 1.6rem; 115 | } 116 | 117 | h2 { 118 | font-size: 1.4rem; 119 | } 120 | 121 | h3 { 122 | font-size: 1.2rem; 123 | } 124 | 125 | b { 126 | font-weight: 700; 127 | } 128 | 129 | #mainWindow { 130 | display: flex; 131 | align-items: flex-start; 132 | justify-content: center; 133 | width: 100%; 134 | height: 100vh; 135 | 136 | padding: 1rem; 137 | padding-top: 6rem; 138 | 139 | overflow-x: auto; 140 | } 141 | 142 | #appWrapper { 143 | display: flex; 144 | flex-direction: row; 145 | } 146 | 147 | #environment-form-container { 148 | max-width: 991px; 149 | width: 100%; 150 | padding: 1rem; 151 | } 152 | 153 | #floatingNotificationsContainer { 154 | position: fixed; 155 | top: 6rem; 156 | right: 2rem; 157 | max-width: 50vw; 158 | 159 | display: flex; 160 | flex-direction: column; 161 | gap: 1rem; 162 | z-index: 30; 163 | } 164 | 165 | /*/ Scroll Bar /*/ 166 | ::-webkit-scrollbar { 167 | width: 0.5rem; 168 | height: 0.5rem; 169 | } 170 | 171 | ::-webkit-scrollbar-track { 172 | background: var(--card); 173 | } 174 | 175 | ::-webkit-scrollbar-thumb { 176 | background: #3f3f46; 177 | border-radius: 0.5rem; 178 | 179 | &:hover { 180 | background: #52525b; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/renderer/assets/styles/pages/AppSettings.view.scss: -------------------------------------------------------------------------------- 1 | #appSettingsContainer { 2 | max-width: 1100px; 3 | 4 | .app-settings-block-container { 5 | width: 100%; 6 | 7 | .settings-card { 8 | display: flex; 9 | gap: 3rem; 10 | flex-direction: column; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/renderer/assets/styles/pages/EnvironmentView.scss: -------------------------------------------------------------------------------- 1 | #center-view-container { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | .empty-server-view { 7 | display: flex; 8 | height: 80vh; 9 | flex-direction: column; 10 | justify-content: center; 11 | align-items: center; 12 | 13 | img.icon { 14 | height: 5rem; 15 | margin: 2rem; 16 | } 17 | } 18 | 19 | .environment-data-container { 20 | display: flex; 21 | gap: 1rem; 22 | justify-content: flex-start; 23 | height: 100%; 24 | 25 | .side-menu { 26 | display: flex; 27 | align-items: flex-start; 28 | justify-content: space-between; 29 | flex-direction: column; 30 | height: 90%; 31 | 32 | min-width: 13rem; 33 | transition: all var(--transition); 34 | 35 | margin-left: -1rem; 36 | padding-left: 1rem; 37 | 38 | &.closed { 39 | min-width: 0rem; 40 | 41 | .menu-items, 42 | .last-menu-items { 43 | .item-text { 44 | font-size: 0rem; 45 | } 46 | } 47 | } 48 | 49 | .menu-items, 50 | .last-menu-items { 51 | display: flex; 52 | align-items: flex-start; 53 | justify-content: space-between; 54 | flex-direction: column; 55 | 56 | gap: 0.5rem; 57 | 58 | width: 100%; 59 | } 60 | 61 | a, 62 | button { 63 | .item-text { 64 | transition: all var(--transition); 65 | color: var(--font-primary); 66 | } 67 | color: var(--font-primary); 68 | font-size: 0.9rem; 69 | border-radius: 0.5rem; 70 | padding: 0.5rem 1.5rem 0.5rem 0rem; 71 | width: 100%; 72 | 73 | display: flex; 74 | align-items: center; 75 | 76 | transition: all var(--transition); 77 | 78 | svg { 79 | margin-right: 0.75rem; 80 | font-size: 1.3rem; 81 | } 82 | 83 | &::before { 84 | width: 0.3rem; 85 | height: 100%; 86 | border-radius: 4px; 87 | content: ''; 88 | background-color: transparent; 89 | 90 | transform: translateX(-0.7rem); 91 | 92 | transition: background-color var(--transition); 93 | } 94 | 95 | &:hover, 96 | &.active { 97 | .item-text { 98 | color: var(--purple); 99 | } 100 | 101 | background-color: var(--card); 102 | color: var(--purple); 103 | box-shadow: var(--card-shadow); 104 | 105 | padding: 0.75rem; 106 | 107 | &::before { 108 | background-color: var(--purple); 109 | } 110 | } 111 | } 112 | } 113 | 114 | #menu-content { 115 | height: inherit; 116 | width: 100%; 117 | overflow: auto; 118 | } 119 | 120 | #environment-edit-form-container { 121 | max-width: 800px; 122 | margin: auto; 123 | padding: 0 1rem; 124 | } 125 | 126 | #environment-summary-container { 127 | display: flex; 128 | justify-content: space-between; 129 | gap: 2rem; 130 | 131 | #server-data { 132 | width: 100%; 133 | } 134 | 135 | #server-info { 136 | min-width: 18rem; 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/renderer/assets/styles/pages/HomeEnvironmentListView.scss: -------------------------------------------------------------------------------- 1 | #homeEnvironmentListContainer { 2 | width: 100%; 3 | } 4 | 5 | #EnvironmentListContent { 6 | width: 100%; 7 | display: flex; 8 | justify-content: flex-start; 9 | } 10 | 11 | .createEnvironmentCard { 12 | background-color: var(--card); 13 | border-radius: var(--card-border-radius); 14 | box-shadow: var(--card-shadow); 15 | padding: 1rem; 16 | display: flex; 17 | gap: 1rem; 18 | 19 | min-height: 15rem; 20 | 21 | .chevron { 22 | display: flex; 23 | align-items: center; 24 | 25 | svg { 26 | height: 1.5rem; 27 | width: 1.5rem; 28 | 29 | color: var(--font-soft); 30 | } 31 | 32 | animation: bounce ease-in-out 2s infinite; 33 | } 34 | 35 | .info { 36 | display: flex; 37 | align-items: center; 38 | justify-content: center; 39 | align-items: center; 40 | flex-direction: column; 41 | 42 | gap: 1rem; 43 | margin-right: 1rem; 44 | max-width: 18rem; 45 | 46 | img { 47 | height: 3rem; 48 | width: 3rem; 49 | } 50 | 51 | span { 52 | text-align: center; 53 | color: var(--font-secondary); 54 | } 55 | } 56 | } 57 | 58 | @keyframes bounce { 59 | 0% { 60 | transform: translateX(0px); 61 | } 62 | 50% { 63 | transform: translateX(1rem); 64 | } 65 | 100% { 66 | transform: translateX(0px); 67 | } 68 | } 69 | 70 | .EnvironmentCard { 71 | height: inherit; 72 | min-height: 15rem; 73 | min-width: 25rem; 74 | max-width: 35rem; 75 | background-color: var(--card); 76 | border-radius: var(--card-border-radius); 77 | box-shadow: var(--card-shadow); 78 | padding: 1rem; 79 | display: flex; 80 | flex-direction: column; 81 | justify-content: space-between; 82 | margin-right: 2rem; 83 | 84 | .heading { 85 | h3 { 86 | margin-bottom: 0; 87 | } 88 | small { 89 | color: var(--font-soft); 90 | font-size: 0.75rem; 91 | } 92 | 93 | display: flex; 94 | justify-content: space-between; 95 | align-items: flex-start; 96 | 97 | .actionButtons { 98 | display: flex; 99 | align-items: center; 100 | justify-content: center; 101 | gap: 1rem; 102 | 103 | button, 104 | a { 105 | text-decoration: none; 106 | color: var(--font-secondary); 107 | padding: 0.5rem; 108 | 109 | svg { 110 | height: 1rem; 111 | width: 1rem; 112 | } 113 | 114 | border-radius: 0.5rem; 115 | background-color: var(--background); 116 | display: flex; 117 | justify-content: center; 118 | align-items: center; 119 | } 120 | } 121 | } 122 | 123 | .graphContainer { 124 | display: flex; 125 | justify-content: center; 126 | align-items: center; 127 | height: 100%; 128 | padding: 0.5rem; 129 | } 130 | 131 | .footer { 132 | display: flex; 133 | justify-content: flex-end; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/renderer/assets/svg/color-server.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/renderer/classes/EnvironmentFormValidator.ts: -------------------------------------------------------------------------------- 1 | import log from 'electron-log'; 2 | import FormValidator from './FormValidator'; 3 | 4 | interface EnvironmentFormData { 5 | name: string; 6 | baseUrl: string; 7 | kind: string; 8 | auth: { 9 | consumerKey: string; 10 | consumerSecret: string; 11 | accessToken: string; 12 | tokenSecret: string; 13 | }; 14 | updateSchedule: { 15 | scrapeFrequency: string; 16 | pingFrequency: string; 17 | }; 18 | } 19 | 20 | export default class EnvironmentFormValidator extends FormValidator { 21 | validate(formData: EnvironmentFormData) { 22 | log.info('EnvironmentFormValidator: Validating form data'); 23 | 24 | if (formData) { 25 | if (formData.name === '') { 26 | this.lastMessage = 'nameIsRequired'; 27 | } else if (formData.baseUrl === '') { 28 | this.lastMessage = 'baseUrlIsRequired'; 29 | } else if (formData.auth.consumerKey === '') { 30 | this.lastMessage = 'consumerKeyIsRequired'; 31 | } else if (formData.auth.consumerSecret === '') { 32 | this.lastMessage = 'consumerSecretIsRequired'; 33 | } else if (formData.auth.accessToken === '') { 34 | this.lastMessage = 'accessTokenIsRequired'; 35 | } else if (formData.auth.tokenSecret === '') { 36 | this.lastMessage = 'tokenSecretIsRequired'; 37 | } else if (formData.updateSchedule.scrapeFrequency === '') { 38 | this.lastMessage = 'scrapeFrequencyIsRequired'; 39 | } else if (formData.updateSchedule.pingFrequency === '') { 40 | this.lastMessage = 'pingFrequencyIsRequired'; 41 | } else { 42 | this.isValid = true; 43 | } 44 | } 45 | 46 | return { isValid: this.isValid, lastMessage: this.lastMessage }; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/renderer/classes/FormValidator.ts: -------------------------------------------------------------------------------- 1 | export default class FormValidator { 2 | /** 3 | * Last helper message from the validator 4 | */ 5 | lastMessage: string; 6 | 7 | /** 8 | * If the form is valid 9 | */ 10 | isValid: boolean; 11 | 12 | constructor() { 13 | this.isValid = false; 14 | this.lastMessage = 'Form not properly validated'; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/renderer/components/base/Box.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | 3 | const Box: React.FC = ({ ...props }) => { 4 | return
{props.children}
; 5 | }; 6 | 7 | export default Box; 8 | -------------------------------------------------------------------------------- /src/renderer/components/base/CreateEnvironmentButton.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/require-default-props */ 2 | import { FiPlus } from 'react-icons/fi'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | import '../../assets/styles/components/CreateEnvironmentButton.scss'; 6 | 7 | type CreateEnvironmentButtonProps = { 8 | isExpanded?: boolean; 9 | }; 10 | 11 | export default function CreateEnvironmentButton({ 12 | isExpanded = false, 13 | }: CreateEnvironmentButtonProps) { 14 | return ( 15 | 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/components/base/DefaultMotionDiv.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { motion } from 'framer-motion'; 3 | 4 | import globalContainerVariants from '../../utils/globalContainerVariants'; 5 | 6 | interface Props { 7 | children: ReactNode; 8 | id: string; 9 | } 10 | 11 | /** 12 | * A framer motion with the default animation from globalContainerVariants 13 | */ 14 | function DefaultMotionDiv({ children, id }: Props) { 15 | return ( 16 | 23 | {children} 24 | 25 | ); 26 | } 27 | 28 | export default DefaultMotionDiv; 29 | -------------------------------------------------------------------------------- /src/renderer/components/base/DynamicImageLoad.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import SpinnerLoader from './Loaders/Spinner'; 3 | 4 | interface Props { 5 | imgSrc: string; 6 | altName: string; 7 | fallback: string; 8 | } 9 | 10 | export default function DynamicImageLoad({ imgSrc, altName, fallback }: Props) { 11 | const [imageIsLoaded, setImageIsLoaded] = useState(false); 12 | const [hasError, setHasError] = useState(false); 13 | 14 | return ( 15 | <> 16 | setImageIsLoaded(true)} 23 | onError={() => { 24 | setHasError(true); 25 | setImageIsLoaded(true); 26 | }} 27 | alt={altName} 28 | /> 29 | 30 | 31 | 32 | Fallback 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/renderer/components/base/EnvironmentFavoriteButton.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | import { AiFillStar, AiOutlineStar } from 'react-icons/ai'; 4 | import { useEnvironmentList } from '../../contexts/EnvironmentListContext'; 5 | import { useNotifications } from '../../contexts/NotificationsContext'; 6 | import { toggleEnvironmentFavorite } from '../../ipc/environmentsIpcHandler'; 7 | 8 | interface Props { 9 | environmentId: number; 10 | isFavorite: boolean; 11 | } 12 | 13 | export default function EnvironmentFavoriteButton({ 14 | environmentId, 15 | isFavorite, 16 | }: Props) { 17 | const { createShortNotification } = useNotifications(); 18 | const { updateEnvironmentList } = useEnvironmentList(); 19 | const [favoriteStar, setFavoriteStar] = useState( 20 | isFavorite ? : 21 | ); 22 | 23 | const { t } = useTranslation(); 24 | 25 | async function toggleFavoriteEnvironment(id: number) { 26 | const { favorited, exception } = await toggleEnvironmentFavorite(id); 27 | 28 | if (exception === 'MAX_FAVORITES_EXCEEDED') { 29 | createShortNotification({ 30 | id: Date.now(), 31 | message: t('helpMessages.environments.maximumExceeded'), 32 | type: 'warning', 33 | }); 34 | 35 | return; 36 | } 37 | 38 | if (favorited) { 39 | createShortNotification({ 40 | id: Date.now(), 41 | message: t('helpMessages.environments.added'), 42 | type: 'success', 43 | }); 44 | setFavoriteStar(); 45 | } else { 46 | createShortNotification({ 47 | id: Date.now(), 48 | message: t('helpMessages.environments.removed'), 49 | type: 'success', 50 | }); 51 | setFavoriteStar(); 52 | } 53 | 54 | updateEnvironmentList(); 55 | } 56 | 57 | return ( 58 | 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /src/renderer/components/base/FloatingNotification.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/require-default-props */ 2 | import { motion } from 'framer-motion'; 3 | import { 4 | FiAlertCircle, 5 | FiAlertOctagon, 6 | FiCheck, 7 | FiInfo, 8 | FiX, 9 | } from 'react-icons/fi'; 10 | import '../../assets/styles/components/FloatingNotification.scss'; 11 | 12 | type FloatingNotificationProps = { 13 | type?: string; 14 | message: string; 15 | mustManuallyClose?: boolean; 16 | }; 17 | 18 | function FloatingNotification({ 19 | type = 'info', 20 | message, 21 | mustManuallyClose = false, 22 | }: FloatingNotificationProps) { 23 | let icon = <>; 24 | const animationVariants = { 25 | hidden: { 26 | opacity: 0, 27 | x: '50vw', 28 | }, 29 | visible: { 30 | opacity: 1, 31 | x: 0, 32 | transition: { ease: 'easeInOut', duration: 0.5 }, 33 | }, 34 | exit: { 35 | opacity: 0, 36 | x: '50vw', 37 | transition: { ease: 'easeInOut', duration: 0.5 }, 38 | }, 39 | }; 40 | 41 | switch (type) { 42 | case 'success': 43 | icon = ; 44 | break; 45 | case 'warning': 46 | icon = ; 47 | break; 48 | case 'error': 49 | icon = ; 50 | break; 51 | default: 52 | icon = ; 53 | break; 54 | } 55 | return ( 56 | 63 |
{icon}
64 |
{message}
65 | {mustManuallyClose ? ( 66 | 69 | ) : ( 70 | <> 71 | )} 72 |
73 | ); 74 | } 75 | 76 | export default FloatingNotification; 77 | -------------------------------------------------------------------------------- /src/renderer/components/base/GraphTooltip.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ValueType, 3 | NameType, 4 | } from 'recharts/types/component/DefaultTooltipContent'; 5 | import { TooltipProps } from 'recharts/types/component/Tooltip'; 6 | import { useTranslation } from 'react-i18next'; 7 | 8 | import '../../assets/styles/components/GraphTooltip.scss'; 9 | import formatBytes from '../../../common/utils/formatBytes'; 10 | 11 | interface Props { 12 | content: TooltipProps; 13 | unit: string; 14 | } 15 | 16 | export default function GraphTooltip({ content, unit }: Props) { 17 | const { t } = useTranslation(); 18 | 19 | if (content && content.active) { 20 | return ( 21 |
22 |

{new Date(content.label).toLocaleString()}

23 |
24 | {content.payload?.map((item) => { 25 | if (item.dataKey) { 26 | let dataKeyTitle = ''; 27 | switch (unit) { 28 | case 'ms': 29 | dataKeyTitle = t('components.GraphTooltip.unitTitles.ms'); 30 | break; 31 | case 'bytes': 32 | dataKeyTitle = t('components.GraphTooltip.unitTitles.bytes'); 33 | break; 34 | default: 35 | break; 36 | } 37 | return ( 38 | 39 | {dataKeyTitle}:{' '} 40 | {unit === 'bytes' 41 | ? formatBytes(item.payload[item.dataKey]) 42 | : `${item.payload[item.dataKey]}${unit}`} 43 | 44 | ); 45 | } 46 | return null; 47 | })} 48 |
49 |
50 | ); 51 | } 52 | 53 | return null; 54 | } 55 | -------------------------------------------------------------------------------- /src/renderer/components/base/Loaders/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import '../../../assets/styles/components/SpinnerLoader.scss'; 2 | 3 | export default function SpinnerLoader() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/renderer/components/base/ProgressBar.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/require-default-props */ 2 | import '../../assets/styles/components/ProgressBar.scss'; 3 | 4 | interface Props { 5 | total: number; 6 | current: number; 7 | showValues?: boolean; 8 | showPercentage?: boolean; 9 | showIndicator?: boolean; 10 | gradient?: boolean; 11 | } 12 | 13 | export default function ProgressBar({ 14 | total, 15 | current, 16 | showValues = false, 17 | showPercentage = false, 18 | showIndicator = true, 19 | gradient = true, 20 | }: Props) { 21 | const percentage = (current / total) * 100; 22 | let bgStyle = ''; 23 | 24 | if (!gradient) { 25 | if (percentage >= 70 && percentage < 90) { 26 | bgStyle = 'var(--yellow)'; 27 | } else if (percentage >= 90) { 28 | bgStyle = 'var(--red)'; 29 | } else { 30 | bgStyle = 'var(--green)'; 31 | } 32 | } 33 | 34 | return ( 35 |
36 | {showValues ? ( 37 |
38 | 0 39 | {total} 40 |
41 | ) : ( 42 | <> 43 | )} 44 | {showIndicator ? ( 45 |
46 | {showPercentage ? `${percentage.toPrecision(3)}%` : current} 47 |
48 | ) : ( 49 | <> 50 | )} 51 |
52 |
59 |
60 |
61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /src/renderer/components/base/SmallTag.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/require-default-props */ 2 | import { useTranslation } from 'react-i18next'; 3 | import '../../assets/styles/components/SmallTag.scss'; 4 | 5 | interface SmallTagInterface { 6 | kind: string; 7 | expanded?: boolean; 8 | } 9 | 10 | export default function SmallTag({ 11 | kind, 12 | expanded = false, 13 | }: SmallTagInterface) { 14 | let className = 'small-tag'; 15 | const { t } = useTranslation(); 16 | 17 | switch (kind) { 18 | case 'PROD': 19 | className += ' is-production'; 20 | break; 21 | case 'HML': 22 | className += ' is-homolog'; 23 | break; 24 | case 'DEV': 25 | className += ' is-dev'; 26 | break; 27 | default: 28 | break; 29 | } 30 | 31 | if (expanded) { 32 | className += ' is-expanded'; 33 | } 34 | 35 | return ( 36 |
37 | {expanded 38 | ? t(`global.environmentKinds.${kind}`) 39 | : t(`global.environmentKindsShort.${kind}`)} 40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/renderer/components/base/Stat/index.scss: -------------------------------------------------------------------------------- 1 | .stat-container { 2 | h3 { 3 | margin: 0; 4 | font-size: 1.75rem; 5 | font-weight: bold; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/renderer/components/base/Stat/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.scss'; 2 | 3 | interface StatProps { 4 | heading: string; 5 | prefix: string; 6 | suffix?: string; 7 | } 8 | 9 | /** 10 | * Stat component. Ideal for displaying a number on dashboards. 11 | * Receives a "heading" property that will display it. 12 | */ 13 | const Stat: React.FC = ({ heading, prefix, suffix }) => { 14 | return ( 15 |
16 | {prefix} 17 |

{heading}

18 | {suffix && {suffix}} 19 |
20 | ); 21 | }; 22 | 23 | export default Stat; 24 | -------------------------------------------------------------------------------- /src/renderer/components/base/TimeIndicator.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/require-default-props */ 2 | import { FiClock } from 'react-icons/fi'; 3 | 4 | interface Props { 5 | date: Date; 6 | mode?: 'FULL' | 'COMPACT' | 'AUTO'; 7 | noMargin?: boolean; 8 | } 9 | 10 | /** 11 | * Renders a time indicator string. 12 | * A date property is required, and will be rendered according to the mode property (defaults to 'AUTO'). 13 | * 14 | * If the mode property is set to AUTO, will render a full datetime when the date is not equal to the current system date. 15 | */ 16 | export default function TimeIndicator({ 17 | date, 18 | mode = 'AUTO', 19 | noMargin = false, 20 | }: Props) { 21 | let dateFormat = ''; 22 | 23 | switch (mode) { 24 | case 'AUTO': 25 | dateFormat = 26 | date.toLocaleDateString() === new Date().toLocaleDateString() 27 | ? date.toLocaleTimeString() 28 | : date.toLocaleString(); 29 | break; 30 | case 'COMPACT': 31 | dateFormat = date.toLocaleTimeString(); 32 | break; 33 | case 'FULL': 34 | dateFormat = date.toLocaleString(); 35 | break; 36 | default: 37 | break; 38 | } 39 | 40 | return ( 41 |
42 | 43 | {dateFormat} 44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/renderer/components/container/DatabasePanel.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | import { useLocation } from 'react-router'; 4 | import { ipcRenderer } from 'electron'; 5 | import { FiArrowDownRight, FiArrowUpRight, FiDatabase } from 'react-icons/fi'; 6 | 7 | import { DbStatistic } from '../../../main/controllers/StatisticsHistoryController'; 8 | import { getLastDatabaseStatistic } from '../../ipc/environmentsIpcHandler'; 9 | import formatBytes from '../../../common/utils/formatBytes'; 10 | import TimeIndicator from '../base/TimeIndicator'; 11 | 12 | /** 13 | * Responsive and environment aware database panel component. 14 | * Uses the useLocation hook to identify the current environment in view. 15 | * @since 0.5 16 | */ 17 | export default function DatabasePanel() { 18 | const [dbInfo, setDbInfo] = useState(null); 19 | const { t } = useTranslation(); 20 | 21 | const location = useLocation(); 22 | const environmentId = location.pathname.split('/')[2]; 23 | 24 | useEffect(() => { 25 | async function getData() { 26 | setDbInfo(await getLastDatabaseStatistic(Number(environmentId))); 27 | } 28 | 29 | ipcRenderer.on(`environmentDataUpdated_${environmentId}`, async () => { 30 | setDbInfo(await getLastDatabaseStatistic(Number(environmentId))); 31 | }); 32 | 33 | getData(); 34 | 35 | return () => { 36 | ipcRenderer.removeAllListeners(`environmentDataUpdated_${environmentId}`); 37 | }; 38 | }, [environmentId]); 39 | 40 | return ( 41 |
42 |
43 |
44 | 45 |
46 | 47 | {t('components.SystemResources.Database.title')} 48 | 49 |
50 | {dbInfo === null ? ( 51 |

{t('components.global.noData')}

52 | ) : ( 53 | <> 54 |
55 |

56 | {t('components.SystemResources.Database.size')} 57 |

58 |

{formatBytes(Number(dbInfo.dbSize))}

59 |
60 |
61 | {Number(dbInfo.dbTraficRecieved) === -1 ? ( 62 |

63 | {t('components.SystemResources.Database.trafficNotAllowed')} 64 |

65 | ) : ( 66 | <> 67 |

68 | {t('components.SystemResources.Database.traffic')} 69 |

70 |
71 |
72 | 73 | {formatBytes(Number(dbInfo.dbTraficRecieved))} 74 |
75 |
76 | 77 | {formatBytes(Number(dbInfo.dbTraficSent))} 78 |
79 |
80 | 81 | )} 82 | 83 | 84 |
85 | 86 | )} 87 |
88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /src/renderer/components/container/DiskPanel.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useLocation } from 'react-router'; 3 | import { FiHardDrive } from 'react-icons/fi'; 4 | import { useTranslation } from 'react-i18next'; 5 | import { ipcRenderer } from 'electron'; 6 | import log from 'electron-log'; 7 | 8 | import formatBytes from '../../../common/utils/formatBytes'; 9 | import { HDStats } from '../../../main/controllers/StatisticsHistoryController'; 10 | import { getDiskInfo } from '../../ipc/environmentsIpcHandler'; 11 | import SpinnerLoader from '../base/Loaders/Spinner'; 12 | import ProgressBar from '../base/ProgressBar'; 13 | import TimeIndicator from '../base/TimeIndicator'; 14 | 15 | /** 16 | * Environment aware self loading disk usage panel component. 17 | * Uses the useLocation hook to identify the current environment in view. 18 | * @since 0.5 19 | */ 20 | function DiskPanel() { 21 | const [diskInfo, setDiskInfo] = useState(null); 22 | const location = useLocation(); 23 | const { t } = useTranslation(); 24 | const environmentId = location.pathname.split('/')[2]; 25 | 26 | useEffect(() => { 27 | async function loadDiskInfo() { 28 | log.info( 29 | `Loading disk data for environment ${environmentId} using the responsive component.` 30 | ); 31 | 32 | const result = await getDiskInfo(Number(environmentId)); 33 | setDiskInfo(result); 34 | } 35 | 36 | ipcRenderer.on(`environmentDataUpdated_${environmentId}`, async () => { 37 | setDiskInfo(await getDiskInfo(Number(environmentId))); 38 | }); 39 | 40 | loadDiskInfo(); 41 | return () => { 42 | ipcRenderer.removeAllListeners(`environmentDataUpdated_${environmentId}`); 43 | setDiskInfo(null); 44 | }; 45 | }, [environmentId]); 46 | 47 | return ( 48 |
49 |
50 |
51 | 52 |
53 | 54 | {t('components.SystemResources.Disk.title')} 55 | 56 |
57 | {diskInfo === null ? ( 58 | 59 | ) : ( 60 | <> 61 | {diskInfo.length === 0 ? ( 62 |

{t('components.global.noData')}

63 | ) : ( 64 | <> 65 |
66 |

67 | {t('components.SystemResources.Disk.used')} 68 |

69 |

70 | {formatBytes( 71 | Number(diskInfo[0].systemServerHDSize) - 72 | Number(diskInfo[0].systemServerHDFree) 73 | )} 74 |

75 |

76 | {t('components.SystemResources.Disk.outOf')}{' '} 77 | {formatBytes(Number(diskInfo[0].systemServerHDSize))} 78 |

79 |
80 |
81 | 92 | 93 | 94 |
95 | 96 | )} 97 | 98 | )} 99 |
100 | ); 101 | } 102 | 103 | export default DiskPanel; 104 | -------------------------------------------------------------------------------- /src/renderer/components/container/EnvironmentLicensesPanel.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | import { useLocation } from 'react-router'; 4 | import { ipcRenderer } from 'electron'; 5 | 6 | import ProgressBar from '../base/ProgressBar'; 7 | import TimeIndicator from '../base/TimeIndicator'; 8 | import { EnvironmentLicenseData } from '../../../main/controllers/LicenseHistoryController'; 9 | import { getLastEnvironmentLicenseData } from '../../ipc/environmentsIpcHandler'; 10 | 11 | /** 12 | * Self loading environment license panel. Uses the current environment id to load the license data. 13 | * @since 0.1.3 14 | */ 15 | export default function EnvironmentLicensesPanel() { 16 | const { t } = useTranslation(); 17 | 18 | const [licenses, setLicenses] = useState(null); 19 | 20 | const location = useLocation(); 21 | const environmentId = location.pathname.split('/')[2]; 22 | 23 | useEffect(() => { 24 | async function loadLicenseData() { 25 | setLicenses(await getLastEnvironmentLicenseData(Number(environmentId))); 26 | } 27 | 28 | ipcRenderer.on(`environmentDataUpdated_${environmentId}`, () => { 29 | loadLicenseData(); 30 | }); 31 | 32 | loadLicenseData(); 33 | return () => { 34 | ipcRenderer.removeAllListeners(`environmentDataUpdated_${environmentId}`); 35 | setLicenses(null); 36 | }; 37 | }, [environmentId]); 38 | 39 | return ( 40 |
41 |

{t('components.EnvironmentLicenses.title')}

42 |
43 | {licenses === null ? ( 44 | {t('components.global.noData')} 45 | ) : ( 46 | <> 47 | 48 | {t('components.EnvironmentLicenses.usedLicenses') 49 | .replace('%active%', String(licenses.activeUsers)) 50 | .replace('%total%', String(licenses.totalLicenses))} 51 | 52 |
53 | 54 | {t('components.EnvironmentLicenses.remainingLicenses').replace( 55 | '%remaining%', 56 | String(licenses.remainingLicenses) 57 | )} 58 | 59 | 68 | 69 | 70 | 71 | )} 72 |
73 |
74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /src/renderer/components/container/EnvironmentName.tsx: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron'; 2 | import { useEffect, useState } from 'react'; 3 | import { useLocation } from 'react-router-dom'; 4 | import { getEnvironmentById } from '../../ipc/environmentsIpcHandler'; 5 | 6 | /** 7 | * Self loading environment name + version component 8 | * @since 0.5 9 | */ 10 | export default function EnvironmentName() { 11 | const [environmentName, setEnvironmentName] = useState(''); 12 | const [release, setEnvironmentRelease] = useState(''); 13 | 14 | const location = useLocation(); 15 | const environmentId = location.pathname.split('/')[2]; 16 | 17 | useEffect(() => { 18 | async function loadEnvironmentName() { 19 | const properties = await getEnvironmentById(Number(environmentId)); 20 | 21 | if (properties) { 22 | setEnvironmentName(properties.name); 23 | setEnvironmentRelease(properties.release); 24 | } 25 | } 26 | 27 | ipcRenderer.on(`environmentDataUpdated_${environmentId}`, () => { 28 | loadEnvironmentName(); 29 | }); 30 | 31 | loadEnvironmentName(); 32 | 33 | return () => { 34 | ipcRenderer.removeAllListeners(`environmentDataUpdated_${environmentId}`); 35 | setEnvironmentName(''); 36 | setEnvironmentRelease(''); 37 | }; 38 | }, [environmentId]); 39 | 40 | return ( 41 |
42 |

43 | {environmentName} Fluig {release} 44 |

45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/renderer/components/container/EnvironmentServicesPanel.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useLocation } from 'react-router-dom'; 3 | import { useTranslation } from 'react-i18next'; 4 | import { ipcRenderer } from 'electron'; 5 | 6 | import '../../assets/styles/components/EnvironmentServices.scss'; 7 | import { EnvironmentServices } from '../../../common/interfaces/EnvironmentControllerInterface'; 8 | import TimeIndicator from '../base/TimeIndicator'; 9 | import { getEnvironmentServices } from '../../ipc/environmentsIpcHandler'; 10 | import getServiceName from '../../utils/getServiceName'; 11 | 12 | export default function EnvironmentServicesPanel() { 13 | const { t } = useTranslation(); 14 | 15 | const location = useLocation(); 16 | const environmentId = location.pathname.split('/')[2]; 17 | 18 | const [environmentServices, setEnvironmentServices] = 19 | useState(null); 20 | const [cardData, setCardData] = useState(<>); 21 | 22 | useEffect(() => { 23 | async function loadServicesData() { 24 | setEnvironmentServices( 25 | await getEnvironmentServices(Number(environmentId)) 26 | ); 27 | } 28 | 29 | ipcRenderer.on(`environmentDataUpdated_${environmentId}`, () => { 30 | loadServicesData(); 31 | }); 32 | 33 | loadServicesData(); 34 | 35 | return () => { 36 | ipcRenderer.removeAllListeners(`environmentDataUpdated_${environmentId}`); 37 | setEnvironmentServices(null); 38 | }; 39 | }, [environmentId]); 40 | 41 | useEffect(() => { 42 | setCardData( 43 | environmentServices ? ( 44 | <> 45 |
46 | {Object.entries(environmentServices.monitorHistory[0]).map( 47 | (item) => { 48 | let status = t('components.EnvironmentServices.failed'); 49 | let className = 'is-failed'; 50 | 51 | if (item[1] === 'OK') { 52 | status = t('components.EnvironmentServices.operational'); 53 | className = 'is-operational'; 54 | } else if (item[1] === 'NONE') { 55 | status = t('components.EnvironmentServices.unused'); 56 | className = 'is-unused'; 57 | } 58 | 59 | if (getServiceName(item[0]) !== 'UNKNOWN') { 60 | return ( 61 |
62 | 63 | {getServiceName(item[0])} 64 | 65 | 66 | {status} 67 | 68 |
69 | ); 70 | } 71 | 72 | return null; 73 | } 74 | )} 75 |
76 | 79 | 80 | ) : ( 81 | {t('components.global.noData')} 82 | ) 83 | ); 84 | }, [environmentServices, t]); 85 | 86 | return ( 87 |
88 |

{t('components.EnvironmentServices.title')}

89 |
{cardData}
90 |
91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /src/renderer/components/container/HomeEnvironmentCard.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { FiSettings } from 'react-icons/fi'; 4 | import { ipcRenderer } from 'electron'; 5 | import { ResponsiveContainer, LineChart, Line } from 'recharts'; 6 | 7 | import { EnvironmentWithRelatedData } from '../../../common/interfaces/EnvironmentControllerInterface'; 8 | import EnvironmentFavoriteButton from '../base/EnvironmentFavoriteButton'; 9 | import SmallTag from '../base/SmallTag'; 10 | import { getEnvironmentById } from '../../ipc/environmentsIpcHandler'; 11 | 12 | interface Props { 13 | injectedEnvironment: EnvironmentWithRelatedData; 14 | } 15 | 16 | function HomeEnvironmentCard({ injectedEnvironment }: Props) { 17 | const [environment, setEnvironment] = 18 | useState(injectedEnvironment); 19 | 20 | useEffect(() => { 21 | ipcRenderer.on(`serverPinged_${environment.id}`, async () => { 22 | setEnvironment(await getEnvironmentById(environment.id, true)); 23 | }); 24 | 25 | return () => { 26 | ipcRenderer.removeAllListeners(`serverPinged_${environment.id}`); 27 | }; 28 | }); 29 | 30 | return ( 31 |
32 |
33 |
34 | 35 |

{environment.name}

36 | {environment.baseUrl} 37 | 38 |
39 |
40 | 44 | 45 | 46 | 47 |
48 |
49 |
50 | {environment.httpResponses.length >= 2 ? ( 51 | 52 | 53 | 0 61 | ? 'var(--blue)' 62 | : 'var(--red)' 63 | } 64 | strokeWidth={2} 65 | /> 66 | 67 | 68 | ) : ( 69 | <> 70 | )} 71 |
72 |
73 | 74 |
75 |
76 | ); 77 | } 78 | 79 | export default HomeEnvironmentCard; 80 | -------------------------------------------------------------------------------- /src/renderer/components/container/MemoryPanel.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useLocation } from 'react-router'; 3 | import { useTranslation } from 'react-i18next'; 4 | import { ipcRenderer } from 'electron'; 5 | import { FiServer } from 'react-icons/fi'; 6 | import log from 'electron-log'; 7 | 8 | import formatBytes from '../../../common/utils/formatBytes'; 9 | import { MemoryStats } from '../../../main/controllers/StatisticsHistoryController'; 10 | import { getMemoryInfo } from '../../ipc/environmentsIpcHandler'; 11 | import SpinnerLoader from '../base/Loaders/Spinner'; 12 | import ProgressBar from '../base/ProgressBar'; 13 | import TimeIndicator from '../base/TimeIndicator'; 14 | 15 | /** 16 | * Environment aware self loading memory panel component. 17 | * Uses the useLocation hook to identify the current environment in view. 18 | * @since 0.5 19 | */ 20 | export default function MemoryPanel() { 21 | const [memoryInfo, setMemoryInfo] = useState(null); 22 | const { t } = useTranslation(); 23 | 24 | const location = useLocation(); 25 | const environmentId = location.pathname.split('/')[2]; 26 | 27 | useEffect(() => { 28 | async function loadMemoryInfo() { 29 | log.info( 30 | `Loading memory data for environment ${environmentId} using the responsive component.` 31 | ); 32 | setMemoryInfo(await getMemoryInfo(Number(environmentId))); 33 | } 34 | 35 | ipcRenderer.on(`environmentDataUpdated_${environmentId}`, async () => { 36 | setMemoryInfo(await getMemoryInfo(Number(environmentId))); 37 | }); 38 | 39 | loadMemoryInfo(); 40 | 41 | return () => { 42 | ipcRenderer.removeAllListeners(`environmentDataUpdated_${environmentId}`); 43 | setMemoryInfo(null); 44 | }; 45 | }, [environmentId]); 46 | 47 | return ( 48 |
49 |
50 |
51 | 52 |
53 | 54 | {t('components.SystemResources.Memory.title')} 55 | 56 |
57 | {memoryInfo === null ? ( 58 | 59 | ) : ( 60 | <> 61 | {memoryInfo.length === 0 ? ( 62 |

{t('components.global.noData')}

63 | ) : ( 64 | <> 65 |
66 |

67 | {t('components.SystemResources.Memory.used')} 68 |

69 |

70 | {formatBytes( 71 | Number(memoryInfo[0].systemServerMemorySize) - 72 | Number(memoryInfo[0].systemServerMemoryFree) 73 | )} 74 |

75 |

76 | {t('components.SystemResources.Memory.outOf')}{' '} 77 | {formatBytes(Number(memoryInfo[0].systemServerMemorySize))} 78 |

79 |
80 |
81 | 92 | 93 | 94 |
95 | 96 | )} 97 | 98 | )} 99 |
100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /src/renderer/components/container/Navbar/EnvironmentList.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from 'framer-motion'; 2 | import { Environment } from '../../../../main/generated/client'; 3 | import EnvironmentListItem from './EnvironmentListItem'; 4 | 5 | type EnvironmentListProps = { 6 | environmentList: Environment[]; 7 | }; 8 | 9 | function EnvironmentList({ environmentList }: EnvironmentListProps) { 10 | let renderList = [] as Environment[]; 11 | 12 | if (environmentList.length > 0) { 13 | renderList = environmentList.filter((env) => env.isFavorite); 14 | } 15 | 16 | if (renderList.length === 0) { 17 | renderList = environmentList; 18 | } 19 | 20 | return renderList.length === 0 ? ( 21 | <> 22 | ) : ( 23 | <> 24 | {renderList.map((environment: Environment, idx: number) => { 25 | return ( 26 | 44 | 49 | 50 | ); 51 | })} 52 | 53 | ); 54 | } 55 | 56 | export default EnvironmentList; 57 | -------------------------------------------------------------------------------- /src/renderer/components/container/Navbar/EnvironmentListItem.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { ipcRenderer } from 'electron'; 3 | import { Link } from 'react-router-dom'; 4 | import { useTranslation } from 'react-i18next'; 5 | import SmallTag from '../../base/SmallTag'; 6 | import { Environment } from '../../../../main/generated/client'; 7 | import '../../../assets/styles/components/EnvironmentListItem.scss'; 8 | 9 | interface EnvironmentListItemInterface { 10 | data: Environment; 11 | isExpanded: boolean; 12 | } 13 | 14 | export default function EnvironmentListItem({ 15 | data, 16 | isExpanded, 17 | }: EnvironmentListItemInterface) { 18 | let environmentKindTitle = ''; 19 | const [isOnline, setIsOnline] = useState(true); 20 | const { t } = useTranslation(); 21 | 22 | ipcRenderer.on(`serverPinged_${data.id}`, (_event, { serverIsOnline }) => { 23 | setIsOnline(serverIsOnline); 24 | }); 25 | 26 | switch (data.kind) { 27 | case 'PROD': 28 | environmentKindTitle = t('global.environmentKinds.PROD'); 29 | break; 30 | case 'HML': 31 | environmentKindTitle = t('global.environmentKinds.HML'); 32 | break; 33 | case 'DEV': 34 | environmentKindTitle = t('global.environmentKinds.DEV'); 35 | break; 36 | 37 | default: 38 | environmentKindTitle = 'Desconhecido (┬┬﹏┬┬)'; 39 | break; 40 | } 41 | 42 | const environmentNameArray = data.name.split(' '); 43 | const environmentInitials = 44 | environmentNameArray.length === 1 45 | ? environmentNameArray[0].substring(0, 2).toUpperCase() 46 | : environmentNameArray[0].substring(0, 1) + 47 | environmentNameArray[1].substring(0, 1); 48 | const environmentTitle = `${data.name} [${ 49 | isOnline ? 'Online' : 'Offline' 50 | }] [${environmentKindTitle}]`; 51 | 52 | if (isExpanded) { 53 | return ( 54 | 59 |
{environmentInitials}
60 |
61 |
{data.name}
62 |
63 |
64 |
65 |
66 | {isOnline ? 'Online' : 'Offline'} 67 |
68 |
69 | 70 |
71 |
72 | 73 | ); 74 | } 75 | 76 | return ( 77 | 82 | {environmentInitials} 83 |
84 |
85 |
86 |
87 | 88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /src/renderer/components/container/Navbar/Logo.tsx: -------------------------------------------------------------------------------- 1 | import { version } from '../../../../../release/app/package.json'; 2 | import logoImage from '../../../assets/img/logo.png'; 3 | 4 | function Logo() { 5 | return ( 6 | <> 7 | Fluig Monitor 8 |
9 | Fluig Monitor 10 | v{version} 11 |
12 | 13 | ); 14 | } 15 | 16 | export default Logo; 17 | -------------------------------------------------------------------------------- /src/renderer/components/container/Navbar/NavActionButtons.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | import { useEffect, useState } from 'react'; 3 | import { ipcRenderer } from 'electron'; 4 | import { Link } from 'react-router-dom'; 5 | import { 6 | FiAirplay, 7 | FiBell, 8 | FiDownload, 9 | FiDownloadCloud, 10 | FiMoon, 11 | FiSettings, 12 | FiSun, 13 | } from 'react-icons/fi'; 14 | import '../../../assets/styles/components/RightButtons.scss'; 15 | import { useTranslation } from 'react-i18next'; 16 | import { useTheme } from '../../../contexts/ThemeContext'; 17 | import SpinnerLoader from '../../base/Loaders/Spinner'; 18 | 19 | export default function NavActionButtons() { 20 | const { t } = useTranslation(); 21 | const { theme, setFrontEndTheme } = useTheme(); 22 | const [themeIcon, setThemeIcon] = useState( 23 | theme === 'DARK' ? : 24 | ); 25 | const [updateActionButton, setUpdateActionButton] = useState(<>); 26 | 27 | function toggleAppTheme() { 28 | if (document.body.classList.contains('dark-theme')) { 29 | setFrontEndTheme('WHITE'); 30 | setThemeIcon(); 31 | } else { 32 | setFrontEndTheme('DARK'); 33 | setThemeIcon(); 34 | } 35 | } 36 | 37 | useEffect(() => { 38 | setThemeIcon(theme === 'DARK' ? : ); 39 | }, [theme]); 40 | 41 | ipcRenderer.on( 42 | 'appUpdateStatusChange', 43 | (_event: Electron.IpcRendererEvent, { status }: { status: string }) => { 44 | if (status === 'AVAILABLE') { 45 | setUpdateActionButton( 46 | 59 | ); 60 | 61 | return; 62 | } 63 | 64 | if (status === 'DOWNLOADED') { 65 | setUpdateActionButton( 66 | 79 | ); 80 | } 81 | } 82 | ); 83 | 84 | return ( 85 |
86 | {updateActionButton} 87 | {/* 97 | */} 107 | 115 | 120 | 121 | 122 |
123 | ); 124 | } 125 | -------------------------------------------------------------------------------- /src/renderer/components/container/Navbar/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | import { AnimatePresence, motion } from 'framer-motion'; 3 | import { useEnvironmentList } from '../../../contexts/EnvironmentListContext'; 4 | import CreateEnvironmentButton from '../../base/CreateEnvironmentButton'; 5 | import EnvironmentList from './EnvironmentList'; 6 | 7 | import '../../../assets/styles/components/Navbar.scss'; 8 | import Logo from './Logo'; 9 | import NavActionButtons from './NavActionButtons'; 10 | 11 | function Navbar() { 12 | const { environmentList } = useEnvironmentList(); 13 | 14 | return ( 15 | 24 |
25 | 26 | 27 | 28 |
29 | 30 | 31 | 32 | 33 |
34 |
35 | 36 |
37 | ); 38 | } 39 | 40 | export default Navbar; 41 | -------------------------------------------------------------------------------- /src/renderer/components/container/SettingsPage/AboutSection.tsx: -------------------------------------------------------------------------------- 1 | import { shell } from 'electron'; 2 | import { FiExternalLink, FiSettings } from 'react-icons/fi'; 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | import { version } from '../../../../../release/app/package.json'; 6 | import bannerLogo from '../../../assets/img/banner_logo.png'; 7 | import DefaultMotionDiv from '../../base/DefaultMotionDiv'; 8 | 9 | export default function AboutSection() { 10 | const { t } = useTranslation(); 11 | 12 | return ( 13 | 14 |

15 | 16 | 17 | 18 | {t('components.AboutSection.headingTitle')} 19 |

20 |
21 |
22 | Fluig Monitor 28 |

Release {version}

29 |
30 |

{t('components.AboutSection.title')}

31 |

32 | {t('components.AboutSection.developedBy')} 33 | 40 | . 41 |

42 |
43 |

{t('components.AboutSection.disclosure')}

44 |

{t('components.AboutSection.usageDisclosure')}

45 |
46 |
47 |

48 | {t('components.AboutSection.learnMoreAt')} 49 | 58 | . 59 |

60 |
61 |
62 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /src/renderer/components/container/SettingsPage/LanguageSettings.tsx: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron'; 2 | import { useTranslation } from 'react-i18next'; 3 | import { FiCompass } from 'react-icons/fi'; 4 | 5 | import DefaultMotionDiv from '../../base/DefaultMotionDiv'; 6 | 7 | export default function LanguageSettings() { 8 | const { t, i18n } = useTranslation(); 9 | 10 | // sends an ipc signal, since the i18n language must be changed on the main process to also 11 | // update the language on the database 12 | function dispatchLanguageChange(lang: string) { 13 | ipcRenderer.invoke('updateLanguage', lang); 14 | } 15 | 16 | return ( 17 | 18 |

19 | 20 | 21 | 22 | {t('components.LanguageSettings.title')} 23 |

24 |

{t('components.LanguageSettings.helperText')}

25 | 26 |
27 | 37 | 47 |
48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/renderer/components/container/SettingsPage/SystemTraySettings.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | import { FiInbox } from 'react-icons/fi'; 4 | 5 | import { 6 | getAppSetting, 7 | updateAppSettings, 8 | } from '../../../ipc/settingsIpcHandler'; 9 | import parseBoolean from '../../../../common/utils/parseBoolean'; 10 | import DefaultMotionDiv from '../../base/DefaultMotionDiv'; 11 | 12 | export default function SystemTraySettings() { 13 | const { t } = useTranslation(); 14 | 15 | const [enableMinimizeFeature, setEnableMinimizeFeature] = useState(false); 16 | const [disableNotification, setDisableNotification] = useState(false); 17 | 18 | function handleEnableMinimizeFeature(checked: boolean) { 19 | setEnableMinimizeFeature(checked); 20 | 21 | updateAppSettings([ 22 | { 23 | settingId: 'ENABLE_MINIMIZE_FEATURE', 24 | value: String(checked), 25 | }, 26 | ]); 27 | } 28 | 29 | function handleDisableNotification(checked: boolean) { 30 | setDisableNotification(checked); 31 | 32 | updateAppSettings([ 33 | { 34 | settingId: 'DISABLE_MINIMIZE_NOTIFICATION', 35 | value: String(checked), 36 | }, 37 | ]); 38 | } 39 | 40 | async function loadSettings() { 41 | const minimizeFeatureSetting = await getAppSetting( 42 | 'ENABLE_MINIMIZE_FEATURE' 43 | ); 44 | 45 | if (minimizeFeatureSetting) { 46 | setEnableMinimizeFeature(parseBoolean(minimizeFeatureSetting.value)); 47 | } 48 | 49 | const disableNotificationSetting = await getAppSetting( 50 | 'DISABLE_MINIMIZE_NOTIFICATION' 51 | ); 52 | 53 | if (disableNotificationSetting) { 54 | setDisableNotification(parseBoolean(disableNotificationSetting.value)); 55 | } 56 | } 57 | 58 | useEffect(() => { 59 | loadSettings(); 60 | }, []); 61 | 62 | return ( 63 | 64 |

65 | 66 | 67 | 68 | {t('components.SystemTraySettings.title')} 69 |

70 | 71 |

{t('components.SystemTraySettings.helperText')}

72 | 73 |
74 | 87 | 88 | 89 | {t('components.SystemTraySettings.minimizeToSystemTrayHelper')} 90 | 91 |
92 | 93 |
94 | 107 | 108 | 109 | {t('components.SystemTraySettings.disableNotificationHelper')} 110 | 111 |
112 |
113 | ); 114 | } 115 | -------------------------------------------------------------------------------- /src/renderer/components/container/SettingsPage/ThemeSettings.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/label-has-associated-control */ 2 | import { useTranslation } from 'react-i18next'; 3 | import { FiPenTool } from 'react-icons/fi'; 4 | 5 | import whiteThemePreview from '../../../assets/img/theme-preview-white.png'; 6 | import darkThemePreview from '../../../assets/img/theme-preview-dark.png'; 7 | import { useTheme } from '../../../contexts/ThemeContext'; 8 | import DefaultMotionDiv from '../../base/DefaultMotionDiv'; 9 | 10 | export default function ThemeSettings() { 11 | const { t } = useTranslation(); 12 | const { theme, setFrontEndTheme } = useTheme(); 13 | 14 | return ( 15 | 16 |

17 | 18 | 19 | 20 | {t('components.ThemeSettings.title')} 21 |

22 |

{t('components.ThemeSettings.helperText')}

23 | 24 |
25 |
26 | 44 |
45 |
46 | 64 |
65 |
66 |
67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /src/renderer/components/container/SettingsPage/UpdatesSettings.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | import { FiInfo, FiPackage } from 'react-icons/fi'; 4 | import { 5 | getAppSettingsAsObject, 6 | updateAppSettings, 7 | } from '../../../ipc/settingsIpcHandler'; 8 | import parseBoolean from '../../../../common/utils/parseBoolean'; 9 | import DefaultMotionDiv from '../../base/DefaultMotionDiv'; 10 | 11 | export default function UpdatesSettings() { 12 | const { t } = useTranslation(); 13 | 14 | const [enableAutoDownload, setEnableAutoDownload] = useState(true); 15 | const [enableAutoInstall, setEnableAutoInstall] = useState(true); 16 | 17 | async function loadSettings() { 18 | const { ENABLE_AUTO_DOWNLOAD_UPDATE, ENABLE_AUTO_INSTALL_UPDATE } = 19 | await getAppSettingsAsObject(); 20 | 21 | setEnableAutoDownload(parseBoolean(ENABLE_AUTO_DOWNLOAD_UPDATE.value)); 22 | setEnableAutoInstall(parseBoolean(ENABLE_AUTO_INSTALL_UPDATE.value)); 23 | } 24 | 25 | function handleEnableUpdateAutoInstall(checked: boolean) { 26 | setEnableAutoInstall(checked); 27 | updateAppSettings([ 28 | { 29 | settingId: 'ENABLE_AUTO_INSTALL_UPDATE', 30 | value: String(checked), 31 | }, 32 | ]); 33 | } 34 | 35 | function handleEnableUpdateAutoDownload(checked: boolean) { 36 | setEnableAutoDownload(checked); 37 | if (!checked) { 38 | handleEnableUpdateAutoInstall(false); 39 | } 40 | updateAppSettings([ 41 | { 42 | settingId: 'ENABLE_AUTO_DOWNLOAD_UPDATE', 43 | value: String(checked), 44 | }, 45 | ]); 46 | } 47 | useEffect(() => { 48 | loadSettings(); 49 | }, []); 50 | 51 | return ( 52 | 53 |

54 | 55 | 56 | 57 | {t('components.UpdatesSettings.title')} 58 |

59 |

{t('components.UpdatesSettings.helperText')}

60 | 61 |
62 | 75 | 76 | 77 | {t('components.UpdatesSettings.enableAutoDownload.helper')} 78 | 79 |
80 | 81 |
82 | 96 | 97 | 98 | {t('components.UpdatesSettings.enableAutoInstall.helper')} 99 | 100 |
101 | 102 |

103 | {t('components.UpdatesSettings.updateFrequency.helper')} 104 |

105 |
106 | ); 107 | } 108 | -------------------------------------------------------------------------------- /src/renderer/components/layout/EnvironmentArtifactsContainer.tsx: -------------------------------------------------------------------------------- 1 | import DefaultMotionDiv from '../base/DefaultMotionDiv'; 2 | 3 | function EnvironmentArtifactsContainer() { 4 | return ( 5 | 6 |

Artefatos

7 |
8 | ); 9 | } 10 | 11 | export default EnvironmentArtifactsContainer; 12 | -------------------------------------------------------------------------------- /src/renderer/components/layout/EnvironmentDatabaseContainer.tsx: -------------------------------------------------------------------------------- 1 | import DatabasePropsPanel from '../container/DatabasePropsPanel'; 2 | import DatabaseStorageGraph from '../container/DatabaseStorageGraph'; 3 | import DatabaseNetworkGraph from '../container/DatabaseNetworkGraph'; 4 | import DefaultMotionDiv from '../base/DefaultMotionDiv'; 5 | 6 | /** 7 | * Environment database info container. Has a 5 x 1 grid template. 8 | * @since 0.5 9 | */ 10 | export default function EnvironmentDatabaseContainer() { 11 | return ( 12 | 13 |
20 |
21 | 22 |
29 | 30 | 31 |
32 | 33 |
34 | 35 |
36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/renderer/components/layout/EnvironmentInsightsContainer.tsx: -------------------------------------------------------------------------------- 1 | import DefaultMotionDiv from '../base/DefaultMotionDiv'; 2 | 3 | function EnvironmentInsightsContainer() { 4 | return ( 5 | 6 |

Insights

7 |
8 | ); 9 | } 10 | 11 | export default EnvironmentInsightsContainer; 12 | -------------------------------------------------------------------------------- /src/renderer/components/layout/EnvironmentRuntimeStatsContainer.tsx: -------------------------------------------------------------------------------- 1 | import DefaultMotionDiv from '../base/DefaultMotionDiv'; 2 | 3 | function EnvironmentRuntimeStatsContainer() { 4 | return ( 5 | 6 |

Estatísticas De Runtime

7 |
8 | ); 9 | } 10 | 11 | export default EnvironmentRuntimeStatsContainer; 12 | -------------------------------------------------------------------------------- /src/renderer/components/layout/EnvironmentServicesContainer.tsx: -------------------------------------------------------------------------------- 1 | import DefaultMotionDiv from '../base/DefaultMotionDiv'; 2 | 3 | function EnvironmentServicesContainer() { 4 | return ( 5 | 6 |

Histórico De Serviços

7 |
8 | ); 9 | } 10 | 11 | export default EnvironmentServicesContainer; 12 | -------------------------------------------------------------------------------- /src/renderer/components/layout/EnvironmentSummaryContainer.tsx: -------------------------------------------------------------------------------- 1 | import DatabasePanel from '../container/DatabasePanel'; 2 | import DiskPanel from '../container/DiskPanel'; 3 | import EnvironmentAvailabilityPanel from '../container/EnvironmentAvailabilityPanel'; 4 | import EnvironmentLicensesPanel from '../container/EnvironmentLicensesPanel'; 5 | import EnvironmentName from '../container/EnvironmentName'; 6 | import EnvironmentPerformanceGraph from '../container/EnvironmentPerformanceGraph'; 7 | import EnvironmentServerInfo from '../container/EnvironmentServerInfo'; 8 | import EnvironmentServicesPanel from '../container/EnvironmentServicesPanel'; 9 | import MemoryPanel from '../container/MemoryPanel'; 10 | import DefaultMotionDiv from '../base/DefaultMotionDiv'; 11 | 12 | /** 13 | * The environment summary view container component. Acts as a container layout for the main components. 14 | * @since 0.1.0 15 | */ 16 | export default function EnvironmentSummary() { 17 | return ( 18 | 19 |
20 |
27 | 28 |
35 | 36 |
45 | 46 | 47 | 48 |
49 |
50 |
51 | 52 |
53 | 54 |
55 | 56 | 57 | 58 |
59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /src/renderer/contexts/EnvironmentListContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, ReactNode, useContext, useState } from 'react'; 2 | import { EnvironmentWithRelatedData } from '../../common/interfaces/EnvironmentControllerInterface'; 3 | import { getAllEnvironments } from '../ipc/environmentsIpcHandler'; 4 | 5 | interface EnvironmentListContextProviderProps { 6 | children: ReactNode; 7 | } 8 | 9 | interface EnvironmentListContextData { 10 | environmentList: EnvironmentWithRelatedData[]; 11 | updateEnvironmentList: () => Promise; 12 | } 13 | 14 | export const EnvironmentListContext = createContext( 15 | {} as EnvironmentListContextData 16 | ); 17 | 18 | export function EnvironmentListContextProvider({ 19 | children, 20 | }: EnvironmentListContextProviderProps) { 21 | const [environmentList, setEnvironmentList] = useState( 22 | [] as EnvironmentWithRelatedData[] 23 | ); 24 | 25 | async function updateEnvironmentList() { 26 | setEnvironmentList(await getAllEnvironments()); 27 | } 28 | 29 | return ( 30 | 36 | {children} 37 | 38 | ); 39 | } 40 | 41 | export const useEnvironmentList = () => { 42 | return useContext(EnvironmentListContext); 43 | }; 44 | -------------------------------------------------------------------------------- /src/renderer/contexts/NotificationsContext.tsx: -------------------------------------------------------------------------------- 1 | import { AnimatePresence } from 'framer-motion'; 2 | import { createContext, ReactNode, useContext, useState } from 'react'; 3 | import FloatingNotification from '../components/base/FloatingNotification'; 4 | 5 | interface NotificationInterface { 6 | id: number; 7 | type: string; 8 | message: string; 9 | } 10 | 11 | interface NotificationsContextProviderProps { 12 | children: ReactNode; 13 | } 14 | interface NotificationsContextData { 15 | notificationList: NotificationInterface[]; 16 | createNotification: ({ id, type, message }: NotificationInterface) => void; 17 | createShortNotification: ( 18 | { id, type, message }: NotificationInterface, 19 | timeout?: number 20 | ) => void; 21 | removeNotification: (id: number) => void; 22 | } 23 | 24 | export const NotificationsContext = createContext( 25 | {} as NotificationsContextData 26 | ); 27 | 28 | export function NotificationsContextProvider({ 29 | children, 30 | }: NotificationsContextProviderProps) { 31 | const [notificationList, setNotificationList] = useState( 32 | [] as NotificationInterface[] 33 | ); 34 | 35 | function createNotification({ id, type, message }: NotificationInterface) { 36 | setNotificationList((prevNotifications) => [ 37 | ...prevNotifications, 38 | { 39 | id, 40 | type, 41 | message, 42 | }, 43 | ]); 44 | } 45 | 46 | function removeNotification(id: number) { 47 | const notificationIndex = notificationList.findIndex( 48 | (item) => item.id === id 49 | ); 50 | if (notificationIndex > -1) { 51 | setNotificationList(notificationList.splice(notificationIndex)); 52 | } 53 | } 54 | 55 | function createShortNotification( 56 | { id, type, message }: NotificationInterface, 57 | timeout = 5000 58 | ) { 59 | createNotification({ id, type, message }); 60 | setTimeout(() => { 61 | // removeNotification(id); 62 | setNotificationList(notificationList.slice(1, notificationList.length)); 63 | }, timeout); 64 | } 65 | 66 | return ( 67 | 75 | {children} 76 |
77 | 78 | {notificationList.map(({ id, type, message }) => { 79 | return ( 80 | 81 | ); 82 | })} 83 | 84 |
85 |
86 | ); 87 | } 88 | 89 | export const useNotifications = () => { 90 | return useContext(NotificationsContext); 91 | }; 92 | -------------------------------------------------------------------------------- /src/renderer/contexts/ThemeContext.tsx: -------------------------------------------------------------------------------- 1 | import log from 'electron-log'; 2 | import { 3 | createContext, 4 | ReactNode, 5 | useContext, 6 | useEffect, 7 | useState, 8 | } from 'react'; 9 | import { getAppSetting, updateAppSettings } from '../ipc/settingsIpcHandler'; 10 | 11 | interface ThemeContextProviderProps { 12 | children: ReactNode; 13 | } 14 | 15 | // Defines the theme context interface (current theme and the switch function) 16 | interface ThemeContextData { 17 | theme: string; 18 | setFrontEndTheme: (theme: string) => void; 19 | } 20 | 21 | // exports the theme context 22 | export const ThemeContext = createContext({} as ThemeContextData); 23 | 24 | // exports the theme context provider, with the current theme value and switch function 25 | export function ThemeContextProvider({ children }: ThemeContextProviderProps) { 26 | const [theme, setTheme] = useState( 27 | document.body.classList.contains('dark-theme') ? 'DARK' : 'WHITE' 28 | ); 29 | 30 | // function that sets the theme to the body and the local database 31 | function setFrontEndTheme(selectedTheme: string, updateDbValue = true) { 32 | log.info(`Updating app front end theme to ${selectedTheme} via context.`); 33 | 34 | if (selectedTheme === 'WHITE') { 35 | document.body.classList.remove('dark-theme'); 36 | } else { 37 | document.body.classList.add('dark-theme'); 38 | } 39 | 40 | setTheme(selectedTheme); 41 | 42 | if (updateDbValue) { 43 | updateAppSettings([ 44 | { 45 | settingId: 'FRONT_END_THEME', 46 | value: selectedTheme, 47 | }, 48 | ]); 49 | } 50 | } 51 | 52 | // loads the theme saved on the database 53 | useEffect(() => { 54 | async function loadThemeFromDb() { 55 | const savedTheme = await getAppSetting('FRONT_END_THEME'); 56 | 57 | if (savedTheme) { 58 | setFrontEndTheme(savedTheme.value, false); 59 | } 60 | } 61 | 62 | loadThemeFromDb(); 63 | }, []); 64 | 65 | return ( 66 | 67 | {children} 68 | 69 | ); 70 | } 71 | 72 | // exports the useTheme method for easy import 73 | export const useTheme = () => { 74 | return useContext(ThemeContext); 75 | }; 76 | -------------------------------------------------------------------------------- /src/renderer/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | Fluig Monitor 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/renderer/index.tsx: -------------------------------------------------------------------------------- 1 | import log from 'electron-log'; 2 | import { ipcRenderer } from 'electron'; 3 | import { Suspense } from 'react'; 4 | import { createRoot } from 'react-dom/client'; 5 | import { I18nextProvider } from 'react-i18next'; 6 | import { HashRouter } from 'react-router-dom'; 7 | import App from './App'; 8 | import i18n from '../common/i18n/i18n'; 9 | 10 | // listens for the custom 'languageChanged' event from main, triggering the language change on the renderer 11 | ipcRenderer.on( 12 | 'languageChanged', 13 | (_event, { language, namespace, resource }) => { 14 | if (!i18n.hasResourceBundle(language, namespace)) { 15 | i18n.addResourceBundle(language, namespace, resource); 16 | } 17 | 18 | i18n.changeLanguage(language); 19 | } 20 | ); 21 | 22 | // ensures that the initial rendered language is the one saved locally 23 | i18n.language = ipcRenderer.sendSync('getLanguage'); 24 | 25 | log.transports.file.fileName = ipcRenderer.sendSync('getIsDevelopment') 26 | ? 'fluig-monitor.dev.log' 27 | : 'fluig-monitor.log'; 28 | log.transports.file.format = ipcRenderer.sendSync('getLogStringFormat'); 29 | 30 | const container = document.getElementById('root'); 31 | if (container) { 32 | createRoot(container).render( 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/renderer/ipc/settingsIpcHandler.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron'; 2 | import { AppSetting } from '../../main/generated/client'; 3 | import { 4 | AppSettingUpdatePropsInterface, 5 | SettingsObject, 6 | } from '../../main/controllers/SettingsController'; 7 | 8 | export async function updateAppSettings( 9 | settings: AppSettingUpdatePropsInterface[] 10 | ): Promise { 11 | const updated = await ipcRenderer.invoke('updateSettings', settings); 12 | 13 | return updated; 14 | } 15 | 16 | export async function getAppSetting( 17 | settingId: string 18 | ): Promise { 19 | const found = await ipcRenderer.invoke('getSetting', settingId); 20 | 21 | return found; 22 | } 23 | 24 | export async function getAppSettingsAsObject(): Promise { 25 | const settings = await ipcRenderer.invoke('getSettingsAsObject'); 26 | 27 | return settings; 28 | } 29 | -------------------------------------------------------------------------------- /src/renderer/pages/AppSettingsView.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from 'react-i18next'; 2 | import SystemTraySettings from '../components/container/SettingsPage/SystemTraySettings'; 3 | import LanguageSettings from '../components/container/SettingsPage/LanguageSettings'; 4 | import AboutSection from '../components/container/SettingsPage/AboutSection'; 5 | import UpdatesSettings from '../components/container/SettingsPage/UpdatesSettings'; 6 | import ThemeSettings from '../components/container/SettingsPage/ThemeSettings'; 7 | 8 | import '../assets/styles/pages/AppSettings.view.scss'; 9 | import DefaultMotionDiv from '../components/base/DefaultMotionDiv'; 10 | 11 | export default function AppSettingsView() { 12 | const { t } = useTranslation(); 13 | 14 | return ( 15 | 16 |

{t('views.AppSettingsView.title')}

17 |
18 |
19 | 20 | 21 | 22 | 23 |
24 |
25 | 26 |
27 |
28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/renderer/pages/HomeEnvironmentListView.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | import { useEffect } from 'react'; 3 | import { FiChevronLeft } from 'react-icons/fi'; 4 | import { useTranslation } from 'react-i18next'; 5 | 6 | import CreateEnvironmentButton from '../components/base/CreateEnvironmentButton'; 7 | import { useEnvironmentList } from '../contexts/EnvironmentListContext'; 8 | 9 | import colorServer from '../assets/svg/color-server.svg'; 10 | import HomeEnvironmentCard from '../components/container/HomeEnvironmentCard'; 11 | 12 | import '../assets/styles/pages/HomeEnvironmentListView.scss'; 13 | import DefaultMotionDiv from '../components/base/DefaultMotionDiv'; 14 | 15 | export default function HomeEnvironmentListView() { 16 | const { environmentList, updateEnvironmentList } = useEnvironmentList(); 17 | const { t } = useTranslation(); 18 | 19 | useEffect(() => { 20 | async function fetchData() { 21 | updateEnvironmentList(); 22 | } 23 | 24 | fetchData(); 25 | }, []); 26 | 27 | const createEnvironmentHelper = ( 28 |
29 |
30 | 31 |
32 |
33 | Server 34 | 35 | {t('views.HomeEnvironmentListView.createEnvironmentHelper')} 36 | 37 |
38 |
39 | ); 40 | 41 | return ( 42 | 43 |

{t('views.HomeEnvironmentListView.header')}

44 |
45 | 46 | {environmentList.length === 0 47 | ? createEnvironmentHelper 48 | : environmentList.map((environment) => ( 49 | 53 | ))} 54 |
55 |
56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /src/renderer/utils/getServiceName.ts: -------------------------------------------------------------------------------- 1 | enum FluigServices { 2 | MSOffice = 'MS Office', 3 | analytics = 'Analytics', 4 | licenseServer = 'License Server', 5 | mailServer = 'Mail Server', 6 | openOffice = 'Open Office', 7 | realTime = 'Fluig Realtime', 8 | solrServer = 'Solr Server', 9 | viewer = 'Fluig Viewer', 10 | unknown = 'UNKNOWN', 11 | } 12 | 13 | /** 14 | * Returns a string with the name of a Fluig Server service. 15 | * @since 0.1.0 16 | */ 17 | export default function getServiceName(id: string): FluigServices { 18 | switch (id) { 19 | case 'MSOffice': 20 | return FluigServices.MSOffice; 21 | case 'analytics': 22 | return FluigServices.analytics; 23 | case 'licenseServer': 24 | return FluigServices.licenseServer; 25 | case 'mailServer': 26 | return FluigServices.mailServer; 27 | case 'openOffice': 28 | return FluigServices.openOffice; 29 | case 'realTime': 30 | return FluigServices.realTime; 31 | case 'solrServer': 32 | return FluigServices.solrServer; 33 | case 'viewer': 34 | return FluigServices.viewer; 35 | default: 36 | return FluigServices.unknown; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/renderer/utils/globalContainerVariants.ts: -------------------------------------------------------------------------------- 1 | const globalContainerVariants = { 2 | hidden: { 3 | opacity: 0, 4 | scale: '90%', 5 | }, 6 | visible: { 7 | opacity: 1, 8 | scale: '100%', 9 | transition: { ease: 'easeInOut', duration: 0.4 }, 10 | }, 11 | exit: { 12 | scale: '90%', 13 | opacity: 0, 14 | transition: { 15 | ease: 'easeInOut', 16 | duration: 0.3, 17 | }, 18 | }, 19 | }; 20 | 21 | export default globalContainerVariants; 22 | -------------------------------------------------------------------------------- /test/utils/commonUtils.spec.ts: -------------------------------------------------------------------------------- 1 | import parseBoolean from '../../src/common/utils/parseBoolean'; 2 | import relativeTime from '../../src/common/utils/relativeTime'; 3 | import formatBytes from '../../src/common/utils/formatBytes'; 4 | import byteSpeed from '../../src/common/utils/byteSpeed'; 5 | import compareSemver from '../../src/common/utils/compareSemver'; 6 | 7 | describe('Common util functions', () => { 8 | describe('Parse boolean util', () => { 9 | it('Parses falsy values', () => { 10 | expect(parseBoolean('false')).toBeFalsy(); 11 | expect(parseBoolean('FALSE')).toBeFalsy(); 12 | expect(parseBoolean(0)).toBeFalsy(); 13 | }); 14 | it('Parses truthy values', () => { 15 | expect(parseBoolean('true')).toBeTruthy(); 16 | expect(parseBoolean('TRUE')).toBeTruthy(); 17 | expect(parseBoolean(1)).toBeTruthy(); 18 | }); 19 | it('Returns false to unknown values', () => { 20 | expect(parseBoolean(null)).toBeFalsy(); 21 | expect(parseBoolean(undefined)).toBeFalsy(); 22 | expect(parseBoolean([1, 2, 3])).toBeFalsy(); 23 | expect(parseBoolean('Sample value')).toBeFalsy(); 24 | }); 25 | }); 26 | 27 | describe('Relative time util', () => { 28 | it('Calculates seconds', () => { 29 | expect(relativeTime(35)).toHaveProperty('seconds', 35); 30 | }); 31 | it('Calculates minutes', () => { 32 | expect(relativeTime(130)).toHaveProperty('minutes', 2); 33 | }); 34 | it('Calculates hours', () => { 35 | expect(relativeTime(11000)).toHaveProperty('hours', 3); 36 | }); 37 | it('Calculates days', () => { 38 | expect(relativeTime(300000)).toHaveProperty('days', 3); 39 | }); 40 | }); 41 | 42 | describe('Format data size util', () => { 43 | it('Formats common data sizes', () => { 44 | expect(formatBytes(1)).toBe('1 Bytes'); 45 | expect(formatBytes(1024)).toBe('1 KB'); 46 | expect(formatBytes(1048576)).toBe('1 MB'); 47 | expect(formatBytes(1073741824)).toBe('1 GB'); 48 | expect(formatBytes(1099511627776)).toBe('1 TB'); 49 | expect(formatBytes(1125899906842624)).toBe('1 PB'); 50 | }); 51 | }); 52 | 53 | describe('Bytes per seconds util', () => { 54 | it('Calculates data speed', () => { 55 | expect(byteSpeed(1024, 1000)).toBe('1 KB/s'); 56 | expect(byteSpeed(1048576, 1000)).toBe('1 MB/s'); 57 | }); 58 | }); 59 | 60 | describe('Compare semantic version util', () => { 61 | it('Compares versions correctly', () => { 62 | expect(compareSemver('1.7.0', '1.6.5')).toBe(1); 63 | expect(compareSemver('1.6.1', '1.6.5')).toBe(-1); 64 | expect(compareSemver('1.8.0', '1.8.0')).toBe(0); 65 | expect(compareSemver('1.8.0', '1.7.1')).toBe(1); 66 | expect(compareSemver('1.5.7', '1.5.14')).toBe(-1); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2021", 4 | "module": "commonjs", 5 | "lib": ["dom", "esnext"], 6 | "declaration": true, 7 | "declarationMap": true, 8 | "jsx": "react-jsx", 9 | "strict": true, 10 | "pretty": true, 11 | "sourceMap": true, 12 | "baseUrl": "./src", 13 | /* Additional Checks */ 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true, 18 | /* Module Resolution Options */ 19 | "moduleResolution": "node", 20 | "esModuleInterop": true, 21 | "allowSyntheticDefaultImports": true, 22 | "resolveJsonModule": true, 23 | "allowJs": true, 24 | "outDir": "release/app/dist" 25 | }, 26 | "exclude": ["test", "release/build", "release/app/dist", ".erb/dll"] 27 | } 28 | --------------------------------------------------------------------------------