├── .erb
├── mocks
│ └── fileMock.js
├── img
│ └── erb-logo.png
├── configs
│ ├── .eslintrc
│ ├── webpack.config.eslint.ts
│ ├── webpack.paths.ts
│ ├── webpack.config.base.ts
│ ├── webpack.config.renderer.dev.dll.ts
│ ├── webpack.config.preload.dev.ts
│ ├── webpack.config.main.prod.ts
│ └── webpack.config.renderer.prod.ts
└── scripts
│ ├── .eslintrc
│ ├── delete-source-maps.js
│ ├── link-modules.ts
│ ├── check-node-env.js
│ ├── clean.js
│ ├── check-port-in-use.js
│ ├── electron-rebuild.js
│ ├── check-build-exists.ts
│ ├── notarize.js
│ └── check-native-dep.js
├── assets
├── icon.ico
├── icon.png
├── icon.icns
├── icons
│ ├── 16x16.png
│ ├── 24x24.png
│ ├── 32x32.png
│ ├── 48x48.png
│ ├── 64x64.png
│ ├── 128x128.png
│ ├── 256x256.png
│ ├── 512x512.png
│ └── 1024x1024.png
├── resources
│ ├── error.png
│ ├── success.png
│ └── warning.png
├── entitlements.mac.plist
└── assets.d.ts
├── docs
└── img
│ ├── banner.png
│ ├── HomeView.png
│ ├── DatabaseView.png
│ ├── DetailedMemoryView.png
│ ├── EnvironmentView_Dark.png
│ ├── EnvironmentView_White.png
│ ├── EnvironmentView_English.png
│ └── EnvironmentView_Unavailable.png
├── release
└── app
│ ├── yarn.lock
│ └── package.json
├── src
├── renderer
│ ├── assets
│ │ ├── img
│ │ │ ├── logo.png
│ │ │ ├── banner_logo.png
│ │ │ ├── defaultServerLogo.png
│ │ │ ├── theme-preview-dark.png
│ │ │ ├── theme-preview-white.png
│ │ │ └── database-logos
│ │ │ │ ├── mysql.png
│ │ │ │ ├── oracle.png
│ │ │ │ ├── database.png
│ │ │ │ └── sql-server.png
│ │ ├── styles
│ │ │ ├── pages
│ │ │ │ ├── AppSettings.view.scss
│ │ │ │ ├── HomeEnvironmentListView.scss
│ │ │ │ └── EnvironmentView.scss
│ │ │ ├── components
│ │ │ │ ├── SpinnerLoader.scss
│ │ │ │ ├── GraphTooltip.scss
│ │ │ │ ├── RightButtons.scss
│ │ │ │ ├── SmallTag.scss
│ │ │ │ ├── EnvironmentServerInfo.scss
│ │ │ │ ├── CreateEnvironmentButton.scss
│ │ │ │ ├── ProgressBar.scss
│ │ │ │ ├── Navbar.scss
│ │ │ │ ├── EnvironmentServices.scss
│ │ │ │ ├── FloatingNotification.scss
│ │ │ │ ├── EnvironmentListItem.scss
│ │ │ │ └── EnvironmentAvailabilityPanel.scss
│ │ │ └── global.scss
│ │ └── svg
│ │ │ └── color-server.svg
│ ├── components
│ │ ├── base
│ │ │ ├── Stat
│ │ │ │ ├── index.scss
│ │ │ │ └── index.tsx
│ │ │ ├── Loaders
│ │ │ │ └── Spinner.tsx
│ │ │ ├── Box.tsx
│ │ │ ├── CreateEnvironmentButton.tsx
│ │ │ ├── DefaultMotionDiv.tsx
│ │ │ ├── SmallTag.tsx
│ │ │ ├── DynamicImageLoad.tsx
│ │ │ ├── TimeIndicator.tsx
│ │ │ ├── ProgressBar.tsx
│ │ │ ├── GraphTooltip.tsx
│ │ │ ├── FloatingNotification.tsx
│ │ │ └── EnvironmentFavoriteButton.tsx
│ │ ├── layout
│ │ │ ├── EnvironmentInsightsContainer.tsx
│ │ │ ├── EnvironmentArtifactsContainer.tsx
│ │ │ ├── EnvironmentServicesContainer.tsx
│ │ │ ├── EnvironmentRuntimeStatsContainer.tsx
│ │ │ ├── EnvironmentDatabaseContainer.tsx
│ │ │ └── EnvironmentSummaryContainer.tsx
│ │ └── container
│ │ │ ├── Navbar
│ │ │ ├── Logo.tsx
│ │ │ ├── Navbar.tsx
│ │ │ ├── EnvironmentList.tsx
│ │ │ ├── EnvironmentListItem.tsx
│ │ │ └── NavActionButtons.tsx
│ │ │ ├── EnvironmentName.tsx
│ │ │ ├── SettingsPage
│ │ │ ├── LanguageSettings.tsx
│ │ │ ├── AboutSection.tsx
│ │ │ ├── ThemeSettings.tsx
│ │ │ ├── UpdatesSettings.tsx
│ │ │ └── SystemTraySettings.tsx
│ │ │ ├── EnvironmentLicensesPanel.tsx
│ │ │ ├── HomeEnvironmentCard.tsx
│ │ │ ├── DatabasePanel.tsx
│ │ │ ├── EnvironmentServicesPanel.tsx
│ │ │ ├── DiskPanel.tsx
│ │ │ └── MemoryPanel.tsx
│ ├── index.ejs
│ ├── classes
│ │ ├── FormValidator.ts
│ │ └── EnvironmentFormValidator.ts
│ ├── utils
│ │ ├── globalContainerVariants.ts
│ │ └── getServiceName.ts
│ ├── ipc
│ │ └── settingsIpcHandler.ts
│ ├── pages
│ │ ├── AppSettingsView.tsx
│ │ └── HomeEnvironmentListView.tsx
│ ├── contexts
│ │ ├── EnvironmentListContext.tsx
│ │ ├── ThemeContext.tsx
│ │ └── NotificationsContext.tsx
│ ├── index.tsx
│ └── App.tsx
├── common
│ ├── interfaces
│ │ ├── FluigVersionApiInterface.ts
│ │ ├── AuthObject.ts
│ │ ├── HttpResponseResourceTypes.ts
│ │ ├── AuthKeysControllerInterface.ts
│ │ ├── UpdateScheduleControllerInterface.ts
│ │ └── EnvironmentControllerInterface.ts
│ ├── i18n
│ │ ├── resources
│ │ │ └── languageResources.ts
│ │ └── i18n.ts
│ ├── utils
│ │ ├── byteSpeed.ts
│ │ ├── parseBoolean.ts
│ │ ├── compareSemver.ts
│ │ ├── formatBytes.ts
│ │ └── relativeTime.ts
│ └── classes
│ │ ├── AuthKeysEncoder.ts
│ │ ├── AuthKeysDecoder.ts
│ │ └── FluigAPIClient.ts
└── main
│ ├── preload.ts
│ ├── interfaces
│ ├── MigrationInterface.ts
│ └── GitHubReleaseInterface.ts
│ ├── utils
│ ├── getAssetPath.ts
│ ├── resolveHtmlPath.ts
│ ├── frequencyToMs.ts
│ ├── fsUtils.ts
│ ├── logSystemConfigs.ts
│ ├── trayBuilder.ts
│ ├── logRotation.ts
│ ├── runPrismaCommand.ts
│ └── globalConstants.ts
│ ├── database
│ ├── prismaContext.ts
│ ├── seedDb.ts
│ └── migrationHandler.ts
│ ├── controllers
│ ├── LogController.ts
│ ├── HttpResponseController.ts
│ ├── LanguageController.ts
│ ├── AuthKeysController.ts
│ ├── UpdateScheduleController.ts
│ ├── MonitorHistoryController.ts
│ └── LicenseHistoryController.ts
│ └── services
│ ├── getEnvironmentRelease.ts
│ └── validateOAuthPermission.ts
├── prisma
└── migrations
│ ├── 20221205230300_create_resource_type_field
│ └── migration.sql
│ └── migration_lock.toml
├── .vscode
├── extensions.json
├── tasks.json
├── launch.json
└── settings.json
├── .editorconfig
├── .gitattributes
├── .env.example
├── .eslintignore
├── scripts
├── copySchema.ts
├── sql
│ └── generateHttpResponses.ts
└── clearDevLogs.ts
├── .gitignore
├── .github
└── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
├── tsconfig.json
├── .eslintrc.js
├── LICENSE
└── test
└── utils
└── commonUtils.spec.ts
/.erb/mocks/fileMock.js:
--------------------------------------------------------------------------------
1 | export default 'test-file-stub';
2 |
--------------------------------------------------------------------------------
/assets/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luizf-lf/fluig-monitor/HEAD/assets/icon.ico
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luizf-lf/fluig-monitor/HEAD/assets/icon.png
--------------------------------------------------------------------------------
/assets/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luizf-lf/fluig-monitor/HEAD/assets/icon.icns
--------------------------------------------------------------------------------
/docs/img/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luizf-lf/fluig-monitor/HEAD/docs/img/banner.png
--------------------------------------------------------------------------------
/.erb/img/erb-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luizf-lf/fluig-monitor/HEAD/.erb/img/erb-logo.png
--------------------------------------------------------------------------------
/assets/icons/16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luizf-lf/fluig-monitor/HEAD/assets/icons/16x16.png
--------------------------------------------------------------------------------
/assets/icons/24x24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luizf-lf/fluig-monitor/HEAD/assets/icons/24x24.png
--------------------------------------------------------------------------------
/assets/icons/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luizf-lf/fluig-monitor/HEAD/assets/icons/32x32.png
--------------------------------------------------------------------------------
/assets/icons/48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luizf-lf/fluig-monitor/HEAD/assets/icons/48x48.png
--------------------------------------------------------------------------------
/assets/icons/64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luizf-lf/fluig-monitor/HEAD/assets/icons/64x64.png
--------------------------------------------------------------------------------
/docs/img/HomeView.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luizf-lf/fluig-monitor/HEAD/docs/img/HomeView.png
--------------------------------------------------------------------------------
/assets/icons/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luizf-lf/fluig-monitor/HEAD/assets/icons/128x128.png
--------------------------------------------------------------------------------
/assets/icons/256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luizf-lf/fluig-monitor/HEAD/assets/icons/256x256.png
--------------------------------------------------------------------------------
/assets/icons/512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luizf-lf/fluig-monitor/HEAD/assets/icons/512x512.png
--------------------------------------------------------------------------------
/docs/img/DatabaseView.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luizf-lf/fluig-monitor/HEAD/docs/img/DatabaseView.png
--------------------------------------------------------------------------------
/assets/icons/1024x1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luizf-lf/fluig-monitor/HEAD/assets/icons/1024x1024.png
--------------------------------------------------------------------------------
/assets/resources/error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luizf-lf/fluig-monitor/HEAD/assets/resources/error.png
--------------------------------------------------------------------------------
/assets/resources/success.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luizf-lf/fluig-monitor/HEAD/assets/resources/success.png
--------------------------------------------------------------------------------
/assets/resources/warning.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luizf-lf/fluig-monitor/HEAD/assets/resources/warning.png
--------------------------------------------------------------------------------
/docs/img/DetailedMemoryView.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luizf-lf/fluig-monitor/HEAD/docs/img/DetailedMemoryView.png
--------------------------------------------------------------------------------
/release/app/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/renderer/assets/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luizf-lf/fluig-monitor/HEAD/src/renderer/assets/img/logo.png
--------------------------------------------------------------------------------
/docs/img/EnvironmentView_Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luizf-lf/fluig-monitor/HEAD/docs/img/EnvironmentView_Dark.png
--------------------------------------------------------------------------------
/docs/img/EnvironmentView_White.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luizf-lf/fluig-monitor/HEAD/docs/img/EnvironmentView_White.png
--------------------------------------------------------------------------------
/docs/img/EnvironmentView_English.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luizf-lf/fluig-monitor/HEAD/docs/img/EnvironmentView_English.png
--------------------------------------------------------------------------------
/docs/img/EnvironmentView_Unavailable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luizf-lf/fluig-monitor/HEAD/docs/img/EnvironmentView_Unavailable.png
--------------------------------------------------------------------------------
/src/renderer/assets/img/banner_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luizf-lf/fluig-monitor/HEAD/src/renderer/assets/img/banner_logo.png
--------------------------------------------------------------------------------
/src/renderer/assets/img/defaultServerLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luizf-lf/fluig-monitor/HEAD/src/renderer/assets/img/defaultServerLogo.png
--------------------------------------------------------------------------------
/src/renderer/assets/img/theme-preview-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luizf-lf/fluig-monitor/HEAD/src/renderer/assets/img/theme-preview-dark.png
--------------------------------------------------------------------------------
/src/renderer/assets/img/theme-preview-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luizf-lf/fluig-monitor/HEAD/src/renderer/assets/img/theme-preview-white.png
--------------------------------------------------------------------------------
/prisma/migrations/20221205230300_create_resource_type_field/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "HTTPResponse" ADD COLUMN "resourceType" TEXT;
3 |
--------------------------------------------------------------------------------
/src/common/interfaces/FluigVersionApiInterface.ts:
--------------------------------------------------------------------------------
1 | export interface FluigVersionApiInterface {
2 | content: string;
3 | message: string | null;
4 | }
5 |
--------------------------------------------------------------------------------
/src/renderer/assets/img/database-logos/mysql.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luizf-lf/fluig-monitor/HEAD/src/renderer/assets/img/database-logos/mysql.png
--------------------------------------------------------------------------------
/src/renderer/assets/img/database-logos/oracle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luizf-lf/fluig-monitor/HEAD/src/renderer/assets/img/database-logos/oracle.png
--------------------------------------------------------------------------------
/src/renderer/assets/img/database-logos/database.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luizf-lf/fluig-monitor/HEAD/src/renderer/assets/img/database-logos/database.png
--------------------------------------------------------------------------------
/.erb/configs/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "no-console": "off",
4 | "global-require": "off",
5 | "import/no-dynamic-require": "off"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/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"
--------------------------------------------------------------------------------
/src/renderer/assets/img/database-logos/sql-server.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luizf-lf/fluig-monitor/HEAD/src/renderer/assets/img/database-logos/sql-server.png
--------------------------------------------------------------------------------
/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/main/preload.ts:
--------------------------------------------------------------------------------
1 | import { contextBridge } from 'electron';
2 |
3 | contextBridge.exposeInMainWorld('electron', {
4 | // functions to be exposed (not yet used)
5 | });
6 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/src/common/interfaces/AuthObject.ts:
--------------------------------------------------------------------------------
1 | export default interface AuthObject {
2 | consumerKey: string;
3 | consumerSecret: string;
4 | accessToken: string;
5 | tokenSecret: string;
6 | }
7 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/src/renderer/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 | Fluig Monitor
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
8 |
9 | Fluig Monitor
10 | v{version}
11 |
12 | >
13 | );
14 | }
15 |
16 | export default Logo;
17 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
38 | >
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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 |
27 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
35 |
36 |
37 | );
38 | }
39 |
40 | export default Navbar;
41 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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 |
60 |
61 | );
62 | }
63 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |

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/assets/svg/color-server.svg:
--------------------------------------------------------------------------------
1 |
29 |
--------------------------------------------------------------------------------
/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/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/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/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |

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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/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 |
59 |
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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 |
66 |
67 | );
68 | }
69 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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 |
87 |
88 | );
89 | }
90 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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 |
95 | >
96 | )}
97 | >
98 | )}
99 |
100 | );
101 | }
102 |
103 | export default DiskPanel;
104 |
--------------------------------------------------------------------------------
/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 |
95 | >
96 | )}
97 | >
98 | )}
99 |
100 | );
101 | }
102 |
--------------------------------------------------------------------------------
/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 |
123 | );
124 | }
125 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------