├── .browserslistrc ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .lintstagedrc.json ├── .nvmrc ├── .postcssrc.js ├── .prettierrc.json ├── .stylelintrc.js ├── .vscode └── settings.json ├── LICENSE ├── Makefile ├── README.md ├── assets ├── assets.d.ts ├── entitlements.mac.plist ├── fonts │ ├── OpenSans-300-1.woff2 │ ├── OpenSans-300-2.woff2 │ ├── OpenSans-300-3.woff2 │ ├── OpenSans-300-4.woff2 │ ├── OpenSans-300-5.woff2 │ ├── OpenSans-300-6.woff2 │ ├── OpenSans-300-7.woff2 │ ├── OpenSans-300i-1.woff2 │ ├── OpenSans-300i-2.woff2 │ ├── OpenSans-300i-3.woff2 │ ├── OpenSans-300i-4.woff2 │ ├── OpenSans-300i-5.woff2 │ ├── OpenSans-300i-6.woff2 │ ├── OpenSans-300i-7.woff2 │ ├── OpenSans-400-1.woff2 │ ├── OpenSans-400-2.woff2 │ ├── OpenSans-400-3.woff2 │ ├── OpenSans-400-4.woff2 │ ├── OpenSans-400-5.woff2 │ ├── OpenSans-400-6.woff2 │ ├── OpenSans-400-7.woff2 │ ├── OpenSans-400i-1.woff2 │ ├── OpenSans-400i-2.woff2 │ ├── OpenSans-400i-3.woff2 │ ├── OpenSans-400i-4.woff2 │ ├── OpenSans-400i-5.woff2 │ ├── OpenSans-400i-6.woff2 │ ├── OpenSans-400i-7.woff2 │ ├── OpenSans-600-1.woff2 │ ├── OpenSans-600-2.woff2 │ ├── OpenSans-600-3.woff2 │ ├── OpenSans-600-4.woff2 │ ├── OpenSans-600-5.woff2 │ ├── OpenSans-600-6.woff2 │ ├── OpenSans-600-7.woff2 │ ├── OpenSans-600i-1.woff2 │ ├── OpenSans-600i-2.woff2 │ ├── OpenSans-600i-3.woff2 │ ├── OpenSans-600i-4.woff2 │ ├── OpenSans-600i-5.woff2 │ ├── OpenSans-600i-6.woff2 │ └── OpenSans-600i-7.woff2 ├── icon.icns ├── icon.ico ├── icon.png ├── icon.svg ├── icons │ ├── 1024x1024.png │ ├── 128x128.png │ ├── 16x16.png │ ├── 24x24.png │ ├── 256x256.png │ ├── 32x32.png │ ├── 48x48.png │ ├── 512x512.png │ ├── 64x64.png │ └── 96x96.png └── screenshots │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ └── 5.png ├── configs ├── mocks │ └── fileMock.js ├── scripts │ ├── .eslintrc │ ├── check-build-exists.ts │ ├── check-native-dep.js │ ├── check-node-env.js │ ├── check-port-in-use.js │ ├── clean.js │ ├── delete-source-maps.js │ ├── electron-rebuild.js │ ├── link-modules.ts │ └── notarize.js └── webpack │ ├── .eslintrc │ ├── webpack.config.base.ts │ ├── webpack.config.eslint.ts │ ├── webpack.config.main.prod.ts │ ├── webpack.config.preload.dev.ts │ ├── webpack.config.renderer.dev.dll.ts │ ├── webpack.config.renderer.dev.ts │ ├── webpack.config.renderer.prod.ts │ └── webpack.paths.ts ├── package-lock.json ├── package.json ├── release └── app │ ├── package-lock.json │ └── package.json ├── src ├── data │ ├── index.ts │ └── types.ts ├── lib │ ├── assert │ │ ├── asserts.ts │ │ ├── guards.ts │ │ └── index.ts │ └── key.ts ├── main │ ├── ipc-renderer │ │ ├── main-base.ts │ │ ├── main.ts │ │ ├── menu │ │ │ ├── main.ts │ │ │ ├── renderer.ts │ │ │ └── types.ts │ │ ├── native-theme │ │ │ ├── main.ts │ │ │ ├── renderer.ts │ │ │ └── types.ts │ │ ├── redis │ │ │ ├── main.ts │ │ │ ├── renderer.ts │ │ │ └── types.ts │ │ ├── renderer-base.ts │ │ ├── renderer.ts │ │ └── types.ts │ ├── lib │ │ ├── redis │ │ │ ├── base-redis.ts │ │ │ ├── configs.ts │ │ │ ├── index.ts │ │ │ ├── ioredis.ts │ │ │ ├── redis.ts │ │ │ ├── tunnel-ssh.ts │ │ │ └── types.ts │ │ └── tunnel-ssh │ │ │ ├── index.ts │ │ │ ├── tunnel-ssh.ts │ │ │ └── types.ts │ ├── main.ts │ ├── menu.ts │ ├── preload.ts │ └── util.ts ├── renderer │ ├── @types │ │ ├── preload.d.ts │ │ ├── styles.d.ts │ │ └── tunnel-ssh.d.ts │ ├── components │ │ └── ask-data-form │ │ │ ├── ask-data-form.pcss │ │ │ ├── ask-data-form.tsx │ │ │ ├── index.ts │ │ │ └── types.ts │ ├── constants │ │ └── app-constants.ts │ ├── data │ │ ├── connections.ts │ │ └── index.ts │ ├── hooks │ │ ├── id-hook.ts │ │ └── index.ts │ ├── index.ejs │ ├── index.tsx │ ├── lib │ │ ├── bem │ │ │ ├── bem.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── db │ │ │ ├── db.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── keyboard.ts │ │ ├── media │ │ │ ├── index.ts │ │ │ ├── mq.ts │ │ │ ├── types.ts │ │ │ ├── use-mq.ts │ │ │ └── utils.ts │ │ ├── mobx.tsx │ │ ├── numbers.ts │ │ ├── page.ts │ │ ├── redis.ts │ │ ├── theme │ │ │ ├── context.ts │ │ │ ├── index.ts │ │ │ ├── provider.tsx │ │ │ ├── styles.ts │ │ │ └── types.ts │ │ └── yup.ts │ ├── mq.json │ ├── scenes │ │ ├── app │ │ │ ├── app.tsx │ │ │ └── index.ts │ │ ├── connection-modal │ │ │ ├── components │ │ │ │ ├── advanced-form │ │ │ │ │ ├── advanced-form.pcss │ │ │ │ │ ├── advanced-form.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── authentication-form │ │ │ │ │ ├── authentication-form.pcss │ │ │ │ │ ├── authentication-form.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── main-form │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── main-form.pcss │ │ │ │ │ └── main-form.tsx │ │ │ │ ├── ssh-form │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── ssh-form.pcss │ │ │ │ │ └── ssh-form.tsx │ │ │ │ ├── test-connect-result │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── test-connect-result.pcss │ │ │ │ │ └── test-connect-result.tsx │ │ │ │ └── tls-form │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── tls-form.pcss │ │ │ │ │ └── tls-form.tsx │ │ │ ├── connection-modal-store.ts │ │ │ ├── connection-modal-view.tsx │ │ │ ├── connection-modal.pcss │ │ │ ├── connection-modal_theme_dark.pcss │ │ │ ├── connection-modal_theme_light.pcss │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── connections-list-modal │ │ │ ├── components │ │ │ │ └── connections-list │ │ │ │ │ ├── components │ │ │ │ │ └── components-list-table │ │ │ │ │ │ ├── components-list-table.pcss │ │ │ │ │ │ ├── components-list-table.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── connections-list.pcss │ │ │ │ │ ├── connections-list.tsx │ │ │ │ │ └── index.ts │ │ │ ├── connections-list-modal-store.ts │ │ │ ├── connections-list-modal-view.tsx │ │ │ ├── connections-list-modal.pcss │ │ │ └── index.ts │ │ ├── edit-value-form │ │ │ ├── edit-value-form-store.ts │ │ │ ├── edit-value-form-view.tsx │ │ │ ├── edit-value-form.pcss │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ └── validation.ts │ │ ├── main-page │ │ │ ├── components │ │ │ │ ├── open-connections-list │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── open-connections-list.pcss │ │ │ │ │ └── open-connections-list.tsx │ │ │ │ ├── resizable-layout │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── resizable-layout.pcss │ │ │ │ │ ├── resizable-layout.tsx │ │ │ │ │ ├── resizable-layout_theme_dark.pcss │ │ │ │ │ └── resizable-layout_theme_light.pcss │ │ │ │ └── top-actions │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── top-actions.pcss │ │ │ │ │ ├── top-actions.tsx │ │ │ │ │ ├── top-actions_theme_dark.pcss │ │ │ │ │ └── top-actions_theme_light.pcss │ │ │ ├── index.ts │ │ │ ├── main-page-store.ts │ │ │ ├── main-page-view.tsx │ │ │ ├── main-page.pcss │ │ │ ├── main-page_theme_dark.pcss │ │ │ └── main-page_theme_light.pcss │ │ └── open-connection │ │ │ ├── index.ts │ │ │ ├── open-connection-store.ts │ │ │ ├── open-connection-view.tsx │ │ │ ├── open-connection.pcss │ │ │ ├── open-connection_theme_dark.pcss │ │ │ ├── open-connection_theme_light.pcss │ │ │ └── types.ts │ ├── stores │ │ ├── config.ts │ │ ├── connection-store.ts │ │ ├── connections-data-store.ts │ │ ├── connections-store.ts │ │ ├── index.ts │ │ ├── root.ts │ │ └── value-tabs-store.ts │ ├── styles │ │ ├── font-family.pcss │ │ ├── reset.pcss │ │ ├── variables.pcss │ │ └── vars │ │ │ ├── border.pcss │ │ │ ├── color.pcss │ │ │ ├── font.pcss │ │ │ ├── grid.pcss │ │ │ └── opacity.pcss │ ├── types │ │ ├── error.ts │ │ ├── index.ts │ │ └── page.ts │ └── ui │ │ ├── button-icon │ │ ├── button-icon.pcss │ │ ├── button-icon.tsx │ │ ├── button-icon_theme_dark.pcss │ │ ├── button-icon_theme_light.pcss │ │ └── index.ts │ │ ├── button │ │ ├── button.pcss │ │ ├── button.tsx │ │ ├── button_theme_dark.pcss │ │ ├── button_theme_light.pcss │ │ └── index.ts │ │ ├── checkbox │ │ ├── checkbox.pcss │ │ ├── checkbox.tsx │ │ ├── checkbox_theme_dark.pcss │ │ ├── checkbox_theme_light.pcss │ │ └── index.ts │ │ ├── formik-field │ │ ├── formik-field.tsx │ │ └── index.ts │ │ ├── heading │ │ ├── heading.pcss │ │ ├── heading.tsx │ │ ├── heading_theme_dark.pcss │ │ ├── heading_theme_light.pcss │ │ └── index.ts │ │ ├── input │ │ ├── index.ts │ │ ├── input.pcss │ │ ├── input.tsx │ │ ├── input_theme_dark.pcss │ │ └── input_theme_light.pcss │ │ ├── label │ │ ├── index.ts │ │ ├── label.pcss │ │ ├── label.tsx │ │ ├── label_theme_dark.pcss │ │ └── label_theme_light.pcss │ │ ├── modal │ │ ├── index.ts │ │ ├── modal.pcss │ │ ├── modal.tsx │ │ ├── modal_theme_dark.pcss │ │ └── modal_theme_light.pcss │ │ ├── number-input │ │ ├── index.ts │ │ └── number-input.tsx │ │ ├── paragraph │ │ ├── index.ts │ │ ├── paragraph.pcss │ │ ├── paragraph.tsx │ │ ├── paragraph_theme_dark.pcss │ │ └── paragraph_theme_light.pcss │ │ ├── password-input │ │ ├── index.ts │ │ └── password-input.tsx │ │ ├── select │ │ ├── index.ts │ │ ├── select.pcss │ │ ├── select.tsx │ │ ├── select_theme_dark.pcss │ │ └── select_theme_light.pcss │ │ ├── spinner │ │ ├── index.ts │ │ ├── spinner.pcss │ │ ├── spinner.tsx │ │ ├── spinner_theme_dark.pcss │ │ └── spinner_theme_light.pcss │ │ ├── table │ │ ├── index.ts │ │ ├── table.pcss │ │ ├── table.tsx │ │ ├── table_theme_dark.pcss │ │ └── table_theme_light.pcss │ │ ├── tabs │ │ ├── index.ts │ │ ├── tabs.pcss │ │ ├── tabs.tsx │ │ ├── tabs_theme_dark.pcss │ │ └── tabs_theme_light.pcss │ │ ├── textarea │ │ ├── index.ts │ │ ├── textarea.pcss │ │ ├── textarea.tsx │ │ ├── textarea_theme_dark.pcss │ │ └── textarea_theme_light.pcss │ │ └── upload-input │ │ ├── index.ts │ │ ├── upload-input.pcss │ │ ├── upload-input.tsx │ │ ├── upload-input_theme_dark.pcss │ │ └── upload-input_theme_light.pcss └── texts.ts └── tsconfig.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | > .5% and last 2 versions 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # postcss 2 | .postcssrc.js 3 | 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | .eslintcache 16 | 17 | # Dependency directory 18 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 19 | node_modules 20 | 21 | # OSX 22 | .DS_Store 23 | 24 | release/app/dist 25 | release/build 26 | configs/dll 27 | 28 | .idea 29 | npm-debug.log.* 30 | *.css.d.ts 31 | *.sass.d.ts 32 | *.scss.d.ts 33 | *.pcss.d.ts 34 | 35 | # eslint ignores hidden directories by default: 36 | # https://github.com/eslint/eslint/issues/8429 37 | !configs 38 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', // Specifies the ESLint parser 3 | extends: [ 4 | 'plugin:react/recommended', 5 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin 6 | 'prettier', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 7 | 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 8 | 'plugin:react-hooks/recommended', 9 | 'plugin:jsx-a11y/recommended', 10 | ], 11 | parserOptions: { 12 | ecmaVersion: 2020, 13 | sourceType: 'module', 14 | project: './tsconfig.json', 15 | createDefaultProgram: true, 16 | tsconfigRootDir: __dirname, 17 | }, 18 | rules: { 19 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs 20 | // e.g. "@typescript-eslint/explicit-function-return-type": "off", 21 | '@typescript-eslint/explicit-function-return-type': ['error', { allowExpressions: true }], 22 | 'react/react-in-jsx-scope': 'off', 23 | }, 24 | settings: { 25 | 'import/resolver': { 26 | // See https://github.com/benmosher/eslint-plugin-import/issues/1396#issuecomment-575727774 for line below 27 | node: {}, 28 | webpack: { 29 | config: require.resolve('./configs/webpack/webpack.config.eslint.ts'), 30 | }, 31 | typescript: {}, 32 | }, 33 | 'import/parsers': { 34 | '@typescript-eslint/parser': ['.ts', '.tsx'], 35 | }, 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /.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 | configs/dll 24 | 25 | .idea 26 | npm-debug.log.* 27 | *.css.d.ts 28 | *.sass.d.ts 29 | *.scss.d.ts 30 | *.pcss.d.ts 31 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "src/**/*.{ts,tsx}": [ 3 | "npx eslint \"src/**/*.{ts,tsx}\"", 4 | "bash -c 'make lint-ts'" 5 | ], 6 | "src/**/*.pcss": [ 7 | "npx stylelint \"src/**/*.pcss\"" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | const postcssImport = require('postcss-import'); 2 | const postcssPresetEnv = require('postcss-preset-env'); 3 | const postcssNested = require('postcss-nested'); 4 | const postcssImportJson = require('@daltontan/postcss-import-json'); 5 | const cssnano = require('cssnano'); 6 | const path = require('path'); 7 | 8 | module.exports = { 9 | plugins: [ 10 | postcssImport({ 11 | path: [ 12 | path.join(__dirname, './src/renderer/styles') 13 | ] 14 | }), 15 | postcssNested, 16 | postcssImportJson(), 17 | postcssPresetEnv({ 18 | stage: 2, 19 | features: { 20 | 'custom-media-queries': true, 21 | 'custom-properties': { 22 | preserve: false, 23 | } 24 | } 25 | }), 26 | cssnano({ 27 | preset: ['default', { 28 | mergeRules: false 29 | }] 30 | }) 31 | ] 32 | }; 33 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "printWidth": 120, 6 | "tabWidth": 2, 7 | "importOrder": [ 8 | "", 9 | "^data$", 10 | "^lib/(.*)$", 11 | "^main/lib/(.*)$", 12 | "^main/constants/(.*)$", 13 | "^main(/.*)$", 14 | "^renderer/types(/.*)?$", 15 | "^renderer/lib/(.*)$", 16 | "^renderer/constants/(.*)$", 17 | "^renderer/data(/.*)?$", 18 | "^renderer/hooks(/.*)?$", 19 | "^renderer/stores(/.*)?$", 20 | "^renderer/ui/(.*)$", 21 | "^renderer/components/(.*)$", 22 | "^renderer/scenes/(.*)$", 23 | "^texts$", 24 | "^[.][.]/(.*)$", 25 | "^[.]/components/(.*)$", 26 | "^([.]|([.]\/(?!.*[.]pcss$)[^\/\\s]+\/?)+)$", 27 | "[.]pcss$" 28 | ], 29 | "importOrderSeparation": true, 30 | "importOrderSortSpecifiers": true, 31 | "importOrderParserPlugins" : ["typescript", "jsx", "classProperties", "decorators"] 32 | } 33 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": [ 3 | "stylelint-config-standard", 4 | "stylelint-config-prettier" 5 | ], 6 | "rules": { 7 | "selector-pseudo-class-no-unknown": [ 8 | true, 9 | { 10 | "ignorePseudoClasses": ["global"] 11 | } 12 | ], 13 | "selector-class-pattern": null, 14 | "custom-property-pattern": null 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll": true 4 | }, 5 | "editor.tabSize": 2, 6 | "editor.insertSpaces": true, 7 | "editor.rulers": [80,120], 8 | "emmet.includeLanguages": { 9 | "postcss": "css" 10 | }, 11 | "emmet.syntaxProfiles": { 12 | "postcss": "css" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Evgeny Zhivitsa 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: deps 2 | deps: 3 | npm ci 4 | 5 | # Build rules 6 | 7 | .PHONY: build 8 | build: 9 | make -j 2 build-main build-renderer 10 | 11 | .PHONY: build-main 12 | build-main: 13 | npx cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./configs/webpack/webpack.config.main.prod.ts 14 | 15 | .PHONY: build-renderer 16 | build-renderer: 17 | npx cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./configs/webpack/webpack.config.renderer.prod.ts 18 | 19 | .PHONY: build-dev-dll-renderer 20 | build-dev-dll-renderer: 21 | npx cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./configs/webpack/webpack.config.renderer.dev.dll.ts 22 | 23 | .PHONY: electron-build 24 | electron-build: 25 | npx electron-builder build --publish never 26 | 27 | .PHONY: electron-rebuild 28 | electron-rebuild: 29 | npx electron-builder build --publish never 30 | 31 | .PHONY: rebuild 32 | rebuild: 33 | npx electron-rebuild --parallel --types prod,dev,optional --module-dir release/app 34 | 35 | # Lint rules 36 | 37 | .PHONY: lint-eslint 38 | lint-eslint: 39 | npx cross-env NODE_ENV=development eslint "src/**/*.{ts,tsx}" 40 | 41 | .PHONY: lint-ts 42 | lint-ts: 43 | npx cross-env NODE_ENV=development npx tsc --noEmit --project tsconfig.json 44 | 45 | .PHONY: lint-stylelint 46 | lint-stylelint: 47 | npx stylelint "src/**/*.pcss" 48 | 49 | .PHONY: lint 50 | lint: lint-eslint lint-ts lint-stylelint 51 | 52 | # Package rules 53 | 54 | .PHONY: clear-dist 55 | clear-dist: 56 | rm -rf dist 57 | 58 | .PHONE: package 59 | package: clear-dist build electron-rebuild 60 | 61 | # Start rules 62 | 63 | .PHONY: start-renderer 64 | start-renderer: 65 | npx cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./configs/webpack/webpack.config.renderer.dev.ts 66 | 67 | .PHONY: start-main 68 | start-main: 69 | npx cross-env NODE_ENV=development electronmon -r ts-node/register/transpile-only . 70 | 71 | .PHONY: start-preload 72 | start-preload: 73 | npx cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./configs/webpack/webpack.config.preload.dev.ts 74 | 75 | .PHONY: check-port 76 | check-port: 77 | npx ts-node ./configs/scripts/check-port-in-use.js 78 | 79 | .PHONY: dev 80 | dev: check-port start-renderer 81 | 82 | # Postinstall rules 83 | 84 | .PHONY: check-native-deps 85 | check-native-deps: 86 | npx ts-node configs/scripts/check-native-dep.js 87 | 88 | .PHONY: app-deps 89 | app-deps: 90 | npm run app-deps 91 | 92 | .PHONY: postinstall 93 | postinstall: check-native-deps app-deps build-dev-dll-renderer 94 | -------------------------------------------------------------------------------- /assets/assets.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const content: any; 3 | export default content; 4 | } 5 | 6 | declare module '*.png' { 7 | const content: any; 8 | export default content; 9 | } 10 | 11 | declare module '*.jpg' { 12 | const content: any; 13 | export default content; 14 | } 15 | -------------------------------------------------------------------------------- /assets/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-unsigned-executable-memory 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /assets/fonts/OpenSans-300-1.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-300-1.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-300-2.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-300-2.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-300-3.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-300-3.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-300-4.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-300-4.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-300-5.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-300-5.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-300-6.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-300-6.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-300-7.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-300-7.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-300i-1.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-300i-1.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-300i-2.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-300i-2.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-300i-3.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-300i-3.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-300i-4.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-300i-4.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-300i-5.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-300i-5.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-300i-6.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-300i-6.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-300i-7.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-300i-7.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-400-1.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-400-1.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-400-2.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-400-2.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-400-3.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-400-3.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-400-4.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-400-4.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-400-5.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-400-5.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-400-6.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-400-6.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-400-7.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-400-7.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-400i-1.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-400i-1.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-400i-2.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-400i-2.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-400i-3.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-400i-3.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-400i-4.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-400i-4.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-400i-5.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-400i-5.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-400i-6.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-400i-6.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-400i-7.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-400i-7.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-600-1.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-600-1.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-600-2.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-600-2.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-600-3.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-600-3.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-600-4.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-600-4.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-600-5.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-600-5.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-600-6.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-600-6.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-600-7.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-600-7.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-600i-1.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-600i-1.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-600i-2.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-600i-2.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-600i-3.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-600i-3.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-600i-4.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-600i-4.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-600i-5.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-600i-5.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-600i-6.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-600i-6.woff2 -------------------------------------------------------------------------------- /assets/fonts/OpenSans-600i-7.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/fonts/OpenSans-600i-7.woff2 -------------------------------------------------------------------------------- /assets/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/icon.icns -------------------------------------------------------------------------------- /assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/icon.ico -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/icon.png -------------------------------------------------------------------------------- /assets/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /assets/icons/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/icons/1024x1024.png -------------------------------------------------------------------------------- /assets/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/icons/128x128.png -------------------------------------------------------------------------------- /assets/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/icons/16x16.png -------------------------------------------------------------------------------- /assets/icons/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/icons/24x24.png -------------------------------------------------------------------------------- /assets/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/icons/256x256.png -------------------------------------------------------------------------------- /assets/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/icons/32x32.png -------------------------------------------------------------------------------- /assets/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/icons/48x48.png -------------------------------------------------------------------------------- /assets/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/icons/512x512.png -------------------------------------------------------------------------------- /assets/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/icons/64x64.png -------------------------------------------------------------------------------- /assets/icons/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/icons/96x96.png -------------------------------------------------------------------------------- /assets/screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/screenshots/1.png -------------------------------------------------------------------------------- /assets/screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/screenshots/2.png -------------------------------------------------------------------------------- /assets/screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/screenshots/3.png -------------------------------------------------------------------------------- /assets/screenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/screenshots/4.png -------------------------------------------------------------------------------- /assets/screenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhivitsa/redis-gui/99c91096ba9f75795ba04fc82e9870bf7871093a/assets/screenshots/5.png -------------------------------------------------------------------------------- /configs/mocks/fileMock.js: -------------------------------------------------------------------------------- 1 | export default 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /configs/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 | -------------------------------------------------------------------------------- /configs/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 '../webpack/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('The main process is not built yet. Build it by running "npm run build:main"'), 13 | ); 14 | } 15 | 16 | if (!fs.existsSync(rendererPath)) { 17 | throw new Error( 18 | chalk.whiteBright.bgRed.bold('The renderer process is not built yet. Build it by running "npm run build:renderer"'), 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /configs/scripts/check-native-dep.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { execSync } from 'child_process'; 3 | import fs from 'fs'; 4 | 5 | import { dependencies } from '../../package.json'; 6 | 7 | if (dependencies) { 8 | const dependenciesKeys = Object.keys(dependencies); 9 | const nativeDeps = fs 10 | .readdirSync('node_modules') 11 | .filter((folder) => fs.existsSync(`node_modules/${folder}/binding.gyp`)); 12 | if (nativeDeps.length === 0) { 13 | process.exit(0); 14 | } 15 | try { 16 | // Find the reason for why the dependency is installed. If it is installed 17 | // because of a devDependency then that is okay. Warn when it is installed 18 | // because of a dependency 19 | const { dependencies: dependenciesObject } = JSON.parse( 20 | execSync(`npm ls ${nativeDeps.join(' ')} --json`).toString(), 21 | ); 22 | const rootDependencies = Object.keys(dependenciesObject); 23 | const filteredRootDependencies = rootDependencies.filter((rootDependency) => 24 | dependenciesKeys.includes(rootDependency), 25 | ); 26 | if (filteredRootDependencies.length > 0) { 27 | const plural = filteredRootDependencies.length > 1; 28 | console.log(` 29 | ${chalk.whiteBright.bgYellow.bold('Webpack does not work with native dependencies.')} 30 | ${chalk.bold(filteredRootDependencies.join(', '))} ${ 31 | plural ? 'are native dependencies' : 'is a native dependency' 32 | } and should be installed inside of the "./release/app" folder. 33 | First, uninstall the packages from "./package.json": 34 | ${chalk.whiteBright.bgGreen.bold('npm uninstall your-package')} 35 | ${chalk.bold('Then, instead of installing the package to the root "./package.json":')} 36 | ${chalk.whiteBright.bgRed.bold('npm install your-package')} 37 | ${chalk.bold('Install the package to "./release/app/package.json"')} 38 | ${chalk.whiteBright.bgGreen.bold('cd ./release/app && npm install your-package')} 39 | Read more about native dependencies at: 40 | ${chalk.bold('https://electron-react-boilerplate.js.org/docs/adding-dependencies/#module-structure')} 41 | `); 42 | process.exit(1); 43 | } 44 | } catch (e) { 45 | console.log('Native dependencies could not be checked'); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /configs/scripts/check-node-env.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | // eslint-disable-next-line @typescript-eslint/explicit-function-return-type 4 | export default function checkNodeEnv(expectedEnv) { 5 | if (!expectedEnv) { 6 | throw new Error('"expectedEnv" not set'); 7 | } 8 | 9 | if (process.env.NODE_ENV !== expectedEnv) { 10 | console.log( 11 | chalk.whiteBright.bgRed.bold(`"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config`), 12 | ); 13 | process.exit(2); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /configs/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 | -------------------------------------------------------------------------------- /configs/scripts/clean.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import rimraf from 'rimraf'; 3 | 4 | import webpackPaths from '../webpack/webpack.paths'; 5 | 6 | const foldersToRemove = [webpackPaths.distPath, webpackPaths.buildPath, webpackPaths.dllPath]; 7 | 8 | foldersToRemove.forEach((folder) => { 9 | if (fs.existsSync(folder)) rimraf.sync(folder); 10 | }); 11 | -------------------------------------------------------------------------------- /configs/scripts/delete-source-maps.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import rimraf from 'rimraf'; 4 | 5 | import webpackPaths from '../webpack/webpack.paths'; 6 | 7 | // eslint-disable-next-line @typescript-eslint/explicit-function-return-type 8 | export default function deleteSourceMaps() { 9 | if (fs.existsSync(webpackPaths.distMainPath)) rimraf.sync(path.join(webpackPaths.distMainPath, '*.js.map')); 10 | if (fs.existsSync(webpackPaths.distRendererPath)) rimraf.sync(path.join(webpackPaths.distRendererPath, '*.js.map')); 11 | } 12 | -------------------------------------------------------------------------------- /configs/scripts/electron-rebuild.js: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import fs from 'fs'; 3 | 4 | import { dependencies } from '../../release/app/package.json'; 5 | import webpackPaths from '../webpack/webpack.paths'; 6 | 7 | if (Object.keys(dependencies || {}).length > 0 && fs.existsSync(webpackPaths.appNodeModulesPath)) { 8 | const electronRebuildCmd = 9 | '../../node_modules/.bin/electron-rebuild --force --types prod,dev,optional --module-dir .'; 10 | const cmd = process.platform === 'win32' ? electronRebuildCmd.replace(/\//g, '\\') : electronRebuildCmd; 11 | execSync(cmd, { 12 | cwd: webpackPaths.appPath, 13 | stdio: 'inherit', 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /configs/scripts/link-modules.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | import webpackPaths from '../webpack/webpack.paths'; 4 | 5 | const { srcNodeModulesPath } = webpackPaths; 6 | const { appNodeModulesPath } = webpackPaths; 7 | 8 | if (!fs.existsSync(srcNodeModulesPath) && fs.existsSync(appNodeModulesPath)) { 9 | fs.symlinkSync(appNodeModulesPath, srcNodeModulesPath, 'junction'); 10 | } 11 | -------------------------------------------------------------------------------- /configs/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 !== 'true') { 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 | -------------------------------------------------------------------------------- /configs/webpack/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": "off", 4 | "global-require": "off", 5 | "import/no-dynamic-require": "off" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /configs/webpack/webpack.config.base.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Base webpack config used across other specific configs 3 | */ 4 | import TsconfigPathsPlugins from 'tsconfig-paths-webpack-plugin'; 5 | import webpack from 'webpack'; 6 | 7 | import { dependencies as externals } from '../../release/app/package.json'; 8 | 9 | import webpackPaths from './webpack.paths'; 10 | 11 | const configuration: webpack.Configuration = { 12 | externals: [...Object.keys(externals || {})], 13 | 14 | stats: 'errors-only', 15 | 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.tsx?$/, 20 | exclude: /node_modules/, 21 | use: { 22 | loader: 'ts-loader', 23 | options: { 24 | // Remove this line to enable type checking in webpack builds 25 | transpileOnly: true, 26 | compilerOptions: { 27 | module: 'esnext', 28 | }, 29 | }, 30 | }, 31 | }, 32 | ], 33 | }, 34 | 35 | output: { 36 | path: webpackPaths.srcPath, 37 | // https://github.com/webpack/webpack/issues/1114 38 | library: { 39 | type: 'commonjs2', 40 | }, 41 | }, 42 | 43 | /** 44 | * Determine the array of extensions that should be used to resolve modules. 45 | */ 46 | resolve: { 47 | extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'], 48 | modules: [webpackPaths.srcPath, 'node_modules'], 49 | // There is no need to add aliases here, the paths in tsconfig get mirrored 50 | plugins: [ 51 | new TsconfigPathsPlugins({ 52 | configFile: webpackPaths.tsConfig, 53 | }), 54 | ], 55 | }, 56 | 57 | plugins: [ 58 | new webpack.EnvironmentPlugin({ 59 | NODE_ENV: 'production', 60 | }), 61 | ], 62 | }; 63 | 64 | export default configuration; 65 | -------------------------------------------------------------------------------- /configs/webpack/webpack.config.eslint.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | module.exports = require('./webpack.config.renderer.dev').default; 3 | -------------------------------------------------------------------------------- /configs/webpack/webpack.config.main.prod.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack config for production electron main process 3 | */ 4 | import path from 'path'; 5 | import TerserPlugin from 'terser-webpack-plugin'; 6 | import webpack from 'webpack'; 7 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 8 | import { merge } from 'webpack-merge'; 9 | 10 | import checkNodeEnv from '../scripts/check-node-env'; 11 | import deleteSourceMaps from '../scripts/delete-source-maps'; 12 | 13 | import baseConfig from './webpack.config.base'; 14 | import webpackPaths from './webpack.paths'; 15 | 16 | checkNodeEnv('production'); 17 | deleteSourceMaps(); 18 | 19 | const configuration: webpack.Configuration = { 20 | devtool: 'source-map', 21 | 22 | mode: 'production', 23 | 24 | target: 'electron-main', 25 | 26 | entry: { 27 | main: path.join(webpackPaths.srcMainPath, 'main.ts'), 28 | preload: path.join(webpackPaths.srcMainPath, 'preload.ts'), 29 | }, 30 | 31 | output: { 32 | path: webpackPaths.distMainPath, 33 | filename: '[name].js', 34 | library: { 35 | type: 'umd', 36 | }, 37 | }, 38 | 39 | optimization: { 40 | minimizer: [ 41 | new TerserPlugin({ 42 | parallel: true, 43 | }), 44 | ], 45 | }, 46 | 47 | plugins: [ 48 | new BundleAnalyzerPlugin({ 49 | analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', 50 | analyzerPort: 8888, 51 | }), 52 | 53 | /** 54 | * Create global constants which can be configured at compile time. 55 | * 56 | * Useful for allowing different behaviour between development builds and 57 | * release builds 58 | * 59 | * NODE_ENV should be production so that modules do not perform certain 60 | * development checks 61 | */ 62 | new webpack.EnvironmentPlugin({ 63 | NODE_ENV: 'production', 64 | DEBUG_PROD: false, 65 | START_MINIMIZED: false, 66 | }), 67 | 68 | new webpack.DefinePlugin({ 69 | 'process.type': '"browser"', 70 | }), 71 | ], 72 | 73 | /** 74 | * Disables webpack processing of __dirname and __filename. 75 | * If you run the bundle in node.js it falls back to these values of node.js. 76 | * https://github.com/webpack/webpack/issues/2010 77 | */ 78 | node: { 79 | __dirname: false, 80 | __filename: false, 81 | }, 82 | }; 83 | 84 | export default merge(baseConfig, configuration); 85 | -------------------------------------------------------------------------------- /configs/webpack/webpack.config.preload.dev.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 4 | import { merge } from 'webpack-merge'; 5 | 6 | import checkNodeEnv from '../scripts/check-node-env'; 7 | 8 | import baseConfig from './webpack.config.base'; 9 | import webpackPaths from './webpack.paths'; 10 | 11 | // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's 12 | // at the dev webpack config is not accidentally run in a production environment 13 | if (process.env.NODE_ENV === 'production') { 14 | checkNodeEnv('development'); 15 | } 16 | 17 | const configuration: webpack.Configuration = { 18 | devtool: 'inline-source-map', 19 | 20 | mode: 'development', 21 | 22 | target: 'electron-preload', 23 | 24 | entry: path.join(webpackPaths.srcMainPath, 'preload.ts'), 25 | 26 | output: { 27 | path: webpackPaths.dllPath, 28 | filename: 'preload.js', 29 | library: { 30 | type: 'umd', 31 | }, 32 | }, 33 | 34 | plugins: [ 35 | new BundleAnalyzerPlugin({ 36 | analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', 37 | }), 38 | 39 | /** 40 | * Create global constants which can be configured at compile time. 41 | * 42 | * Useful for allowing different behaviour between development builds and 43 | * release builds 44 | * 45 | * NODE_ENV should be production so that modules do not perform certain 46 | * development checks 47 | * 48 | * By default, use 'development' as NODE_ENV. This can be overriden with 49 | * 'staging', for example, by changing the ENV variables in the npm scripts 50 | */ 51 | new webpack.EnvironmentPlugin({ 52 | NODE_ENV: 'development', 53 | }), 54 | 55 | new webpack.LoaderOptionsPlugin({ 56 | debug: true, 57 | }), 58 | ], 59 | 60 | /** 61 | * Disables webpack processing of __dirname and __filename. 62 | * If you run the bundle in node.js it falls back to these values of node.js. 63 | * https://github.com/webpack/webpack/issues/2010 64 | */ 65 | node: { 66 | __dirname: false, 67 | __filename: false, 68 | }, 69 | 70 | watch: true, 71 | }; 72 | 73 | export default merge(baseConfig, configuration); 74 | -------------------------------------------------------------------------------- /configs/webpack/webpack.config.renderer.dev.dll.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | 3 | /** 4 | * Builds the DLL for development electron renderer process 5 | */ 6 | import path from 'path'; 7 | import webpack from 'webpack'; 8 | import { merge } from 'webpack-merge'; 9 | 10 | import { dependencies } from '../../package.json'; 11 | import checkNodeEnv from '../scripts/check-node-env'; 12 | 13 | import baseConfig from './webpack.config.base'; 14 | import webpackPaths from './webpack.paths'; 15 | 16 | checkNodeEnv('development'); 17 | 18 | const dist = webpackPaths.dllPath; 19 | 20 | const configuration: webpack.Configuration = { 21 | context: webpackPaths.rootPath, 22 | 23 | devtool: 'eval', 24 | 25 | mode: 'development', 26 | 27 | target: 'electron-renderer', 28 | 29 | externals: ['fsevents', 'crypto-browserify'], 30 | 31 | /** 32 | * Use `module` from `webpack.config.renderer.dev.js` 33 | */ 34 | module: require('./webpack.config.renderer.dev').default.module, 35 | 36 | entry: { 37 | renderer: Object.keys(dependencies || {}), 38 | }, 39 | 40 | output: { 41 | path: dist, 42 | filename: '[name].dev.dll.js', 43 | library: { 44 | name: 'renderer', 45 | type: 'var', 46 | }, 47 | }, 48 | 49 | plugins: [ 50 | new webpack.DllPlugin({ 51 | path: path.join(dist, '[name].json'), 52 | name: '[name]', 53 | }), 54 | 55 | /** 56 | * Create global constants which can be configured at compile time. 57 | * 58 | * Useful for allowing different behaviour between development builds and 59 | * release builds 60 | * 61 | * NODE_ENV should be production so that modules do not perform certain 62 | * development checks 63 | */ 64 | new webpack.EnvironmentPlugin({ 65 | NODE_ENV: 'development', 66 | }), 67 | 68 | new webpack.LoaderOptionsPlugin({ 69 | debug: true, 70 | options: { 71 | context: webpackPaths.srcPath, 72 | output: { 73 | path: webpackPaths.dllPath, 74 | }, 75 | }, 76 | }), 77 | ], 78 | }; 79 | 80 | export default merge(baseConfig, configuration); 81 | -------------------------------------------------------------------------------- /configs/webpack/webpack.paths.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const path = require('path'); 3 | 4 | const rootPath = path.join(__dirname, '../..'); 5 | 6 | const dllPath = path.join(__dirname, '../dll'); 7 | 8 | const srcPath = path.join(rootPath, 'src'); 9 | const srcMainPath = path.join(srcPath, 'main'); 10 | const srcRendererPath = path.join(srcPath, 'renderer'); 11 | 12 | const releasePath = path.join(rootPath, 'release'); 13 | const appPath = path.join(releasePath, 'app'); 14 | const appPackagePath = path.join(appPath, 'package.json'); 15 | const appNodeModulesPath = path.join(appPath, 'node_modules'); 16 | const srcNodeModulesPath = path.join(srcPath, 'node_modules'); 17 | 18 | const distPath = path.join(appPath, 'dist'); 19 | const distMainPath = path.join(distPath, 'main'); 20 | const distRendererPath = path.join(distPath, 'renderer'); 21 | 22 | const buildPath = path.join(releasePath, 'build'); 23 | 24 | const tsConfig = path.join(rootPath, 'tsconfig.json'); 25 | 26 | export default { 27 | rootPath, 28 | dllPath, 29 | srcPath, 30 | srcMainPath, 31 | srcRendererPath, 32 | releasePath, 33 | appPath, 34 | appPackagePath, 35 | appNodeModulesPath, 36 | srcNodeModulesPath, 37 | distPath, 38 | distMainPath, 39 | distRendererPath, 40 | buildPath, 41 | tsConfig, 42 | }; 43 | -------------------------------------------------------------------------------- /release/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redis-gui", 3 | "version": "4.6.0", 4 | "license": "MIT", 5 | "author": "Evgeny Zhivitsa ", 6 | "main": "./dist/main/main.js", 7 | "scripts": { 8 | "rebuild": "node -r ts-node/register ../../configs/scripts/electron-rebuild.js", 9 | "postinstall": "npm run rebuild && npm run link-modules", 10 | "link-modules": "node -r ts-node/register ../../configs/scripts/link-modules.ts" 11 | }, 12 | "dependencies": { 13 | "ioredis": "5.3.1", 14 | "ssh2": "1.11.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/data/index.ts: -------------------------------------------------------------------------------- 1 | export { ConnectionType, SshAuthMethod, AuthenticationMethod, InvalidHostnames } from './types'; 2 | export type { 3 | Connection, 4 | ConnectionData, 5 | FileData, 6 | ConnectionSsh, 7 | ConnectionMain, 8 | ConnectionAuth, 9 | ConnectionTls, 10 | ConnectionAdvanced, 11 | } from './types'; 12 | -------------------------------------------------------------------------------- /src/data/types.ts: -------------------------------------------------------------------------------- 1 | import { DBSchema } from 'idb'; 2 | 3 | export enum AuthenticationMethod { 4 | SelfSigned = 'self-signed', 5 | CaCertificate = 'ca-certificate', 6 | } 7 | 8 | export enum InvalidHostnames { 9 | NotAllowed = 'not-allowed', 10 | Allowed = 'allowed', 11 | } 12 | 13 | export enum SshAuthMethod { 14 | PrivateKey = 'private-key', 15 | Password = 'password', 16 | } 17 | 18 | export enum ConnectionType { 19 | Direct = 'direct', 20 | Cluster = 'cluster', 21 | Sentinel = 'sentinel', 22 | } 23 | 24 | export interface FileData { 25 | name: string; 26 | text: string; 27 | } 28 | 29 | export interface ConnectionTls { 30 | enabled: boolean; 31 | authenticationMethod: AuthenticationMethod; 32 | ca?: FileData; 33 | usePem: boolean; 34 | pem?: FileData; 35 | passphrase: string; 36 | askForPassphraseEachTime: boolean; 37 | advancedOptions: boolean; 38 | crl?: FileData; 39 | invalidHostnames: InvalidHostnames; 40 | } 41 | 42 | export interface ConnectionSsh { 43 | enabled: boolean; 44 | host: string; 45 | port: string; 46 | username: string; 47 | authMethod: SshAuthMethod; 48 | privateKey?: FileData; 49 | passphrase: string; 50 | askForPassphraseEachTime: boolean; 51 | password: string; 52 | askForPasswordEachTime: boolean; 53 | } 54 | 55 | export interface ConnectionData { 56 | port: string; 57 | host: string; 58 | } 59 | 60 | export interface ConnectionMain { 61 | name: string; 62 | type: ConnectionType; 63 | addresses: ConnectionData[]; 64 | sentinelName: string; 65 | readOnly: boolean; 66 | } 67 | 68 | export interface ConnectionAuth { 69 | performAuth: boolean; 70 | password: string; 71 | username: string; 72 | sentinelPassword: string; 73 | sentinelUsername: string; 74 | } 75 | 76 | export interface ConnectionAdvanced { 77 | family: number; 78 | db: number; 79 | keyPrefix: string; 80 | stringNumbers: boolean; 81 | } 82 | 83 | export interface Connection { 84 | id: string; 85 | main: ConnectionMain; 86 | auth: ConnectionAuth; 87 | ssh: ConnectionSsh; 88 | tls: ConnectionTls; 89 | advanced: ConnectionAdvanced; 90 | } 91 | 92 | export interface Db extends DBSchema { 93 | connections: { 94 | value: Connection; 95 | key: string; 96 | }; 97 | } 98 | -------------------------------------------------------------------------------- /src/lib/assert/asserts.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Exclude null and undefined from T 3 | */ 4 | type NonNullable = T & object; 5 | 6 | export function assertExists(arg: T): asserts arg is NonNullable { 7 | if (arg === null || arg === undefined) { 8 | throw new Error(`Argument ${arg} is not valid`); 9 | } 10 | } 11 | 12 | export function castExists(arg: T): NonNullable { 13 | assertExists(arg); 14 | return arg; 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/assert/guards.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * isNumber is a type guard for the number type. 3 | */ 4 | export const isNumber = (arg: unknown): arg is number => { 5 | return typeof arg === 'number'; 6 | }; 7 | 8 | /** 9 | * isString is a type guard for the string type. Note that this says nothing 10 | * about the length of strings (e.g. '' is still a string). 11 | */ 12 | export const isString = (arg: unknown): arg is string => typeof arg === 'string'; 13 | 14 | /** 15 | * isBoolean is a type guard for the boolean type. Note that this is different 16 | * from truthiness - see the `exists` guard in ./asserts for the closest 17 | * equivalent. 18 | */ 19 | export const isBoolean = (arg: unknown): arg is boolean => typeof arg === 'boolean'; 20 | 21 | /** 22 | * isNull is a type guard for the null primitive 23 | */ 24 | export const isNull = (arg: unknown): arg is null => arg === null; 25 | 26 | /** 27 | * isUndefined is a type guard for the undefined primitive 28 | */ 29 | export const isUndefined = (arg: unknown): arg is undefined => arg === undefined; 30 | 31 | /** 32 | * isObject is a type guard for Objects. 33 | */ 34 | export const isObject = (arg: unknown): arg is object => 35 | !!arg && (typeof arg === 'object' || typeof arg === 'function'); 36 | -------------------------------------------------------------------------------- /src/lib/assert/index.ts: -------------------------------------------------------------------------------- 1 | export * from './guards'; 2 | export * from './asserts'; 3 | -------------------------------------------------------------------------------- /src/lib/key.ts: -------------------------------------------------------------------------------- 1 | export const REDIS_PREFIX_SEPARATOR = ':'; 2 | 3 | export function listToKey(items: string[] = []): string { 4 | return items.join(REDIS_PREFIX_SEPARATOR); 5 | } 6 | 7 | export function keyToList(key: string): string[] { 8 | return key ? key.split(REDIS_PREFIX_SEPARATOR) : []; 9 | } 10 | 11 | export function hasPrefix(key: string): boolean { 12 | return key.includes(REDIS_PREFIX_SEPARATOR); 13 | } 14 | 15 | export function parseKey(key: string, prefix: string): { key: string; prefix?: string } { 16 | const keyEnd = prefix && key.startsWith(prefix) ? key.substring(prefix.length + 1) : key; 17 | const isKey = !keyEnd.includes(REDIS_PREFIX_SEPARATOR); 18 | 19 | if (isKey) { 20 | return { key: keyEnd }; 21 | } else { 22 | const parts = keyToList(keyEnd); 23 | const resultPrefix = parts.slice(0, parts.length - 1); 24 | const resultKey = parts[parts.length - 1]; 25 | const resultPrefixStr = listToKey(resultPrefix); 26 | 27 | return { prefix: resultPrefixStr, key: resultKey }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/ipc-renderer/main-base.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, IpcMainEvent, IpcMainInvokeEvent, ipcMain } from 'electron'; 2 | 3 | import { 4 | BaseEvent, 5 | BaseInvokeEvent, 6 | EventAnyResponse, 7 | EventData, 8 | EventHandler, 9 | EventType, 10 | InvokeListener, 11 | IpcMainBase, 12 | UnsubscribeFn, 13 | } from './types'; 14 | 15 | export function getIpcMainBase< 16 | TToRenderer extends BaseEvent, 17 | TFromRenderer extends BaseEvent, 18 | TInvokeFromRendererData extends BaseInvokeEvent, 19 | >(channel: string): IpcMainBase { 20 | let mainBrowserWindow: BrowserWindow | undefined; 21 | 22 | const invokeEventHandlers = new Map< 23 | string, 24 | (data: EventAnyResponse) => Promise> 25 | >(); 26 | const invokeHandler = ( 27 | _event: IpcMainInvokeEvent, 28 | data: TInvokeFromRendererData, 29 | ): Promise> => { 30 | const eventType = data.type; 31 | const handler = invokeEventHandlers.get(eventType); 32 | if (!handler) { 33 | throw new Error(`handler for {eventType} not found`); 34 | } 35 | 36 | return handler(data.data); 37 | }; 38 | 39 | return { 40 | initialize(mainWindow: BrowserWindow): void { 41 | mainBrowserWindow = mainWindow; 42 | ipcMain.handle(channel, invokeHandler); 43 | }, 44 | 45 | destroy(): void { 46 | ipcMain.removeHandler(channel); 47 | }, 48 | 49 | on>( 50 | eventType: Type, 51 | func: EventHandler>, 52 | ): UnsubscribeFn { 53 | const subscription = (_event: IpcMainEvent, data: TFromRenderer): void => { 54 | if (data.type === eventType) { 55 | func(data.data); 56 | } 57 | }; 58 | ipcMain.on(channel, subscription); 59 | 60 | return () => { 61 | ipcMain.removeListener(channel, subscription); 62 | }; 63 | }, 64 | 65 | once>( 66 | eventType: Type, 67 | func: EventHandler>, 68 | ): void { 69 | const subscription = (_event: IpcMainEvent, data: TFromRenderer): void => { 70 | if (data.type === eventType) { 71 | func(data.data); 72 | } 73 | }; 74 | ipcMain.once(channel, subscription); 75 | }, 76 | 77 | sendMessage(data: TToRenderer): void { 78 | if (!mainBrowserWindow) { 79 | return; 80 | } 81 | 82 | mainBrowserWindow.webContents.send(channel, data); 83 | }, 84 | 85 | handle>( 86 | eventType: Type, 87 | handler: InvokeListener, 88 | ): void { 89 | invokeEventHandlers.set(eventType, handler as any); 90 | }, 91 | }; 92 | } 93 | -------------------------------------------------------------------------------- /src/main/ipc-renderer/main.ts: -------------------------------------------------------------------------------- 1 | export * from './native-theme/main'; 2 | export * from './menu/main'; 3 | export * from './redis/main'; 4 | -------------------------------------------------------------------------------- /src/main/ipc-renderer/menu/main.ts: -------------------------------------------------------------------------------- 1 | import { getIpcMainBase } from '../main-base'; 2 | import { Channel } from '../types'; 3 | 4 | import { MenuToRendererData } from './types'; 5 | 6 | export const menuMain = getIpcMainBase(Channel.MENU); 7 | -------------------------------------------------------------------------------- /src/main/ipc-renderer/menu/renderer.ts: -------------------------------------------------------------------------------- 1 | import { getBaseIpcRenderer } from '../renderer-base'; 2 | import { Channel } from '../types'; 3 | 4 | import { MenuToRendererData } from './types'; 5 | 6 | export const menuRenderer = getBaseIpcRenderer(Channel.MENU); 7 | -------------------------------------------------------------------------------- /src/main/ipc-renderer/menu/types.ts: -------------------------------------------------------------------------------- 1 | import { BaseEvent } from '../types'; 2 | 3 | export type MenuToRendererData = BaseEvent<'OPEN_CONNECTIONS_LIST', void>; 4 | -------------------------------------------------------------------------------- /src/main/ipc-renderer/native-theme/main.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, nativeTheme } from 'electron'; 2 | 3 | import { getIpcMainBase } from '../main-base'; 4 | import { Channel, IpcMainBase } from '../types'; 5 | 6 | import { NativeThemeData, NativeThemeInvokeData, NativeThemeToRendererData } from './types'; 7 | 8 | const ipcMainBase = getIpcMainBase(Channel.NATIVE_THEME); 9 | 10 | function sendDataToHost(): void { 11 | ipcMainBase.sendMessage({ 12 | type: 'NATIVE_THEME_UPDATED', 13 | data: { 14 | shouldUseDarkColors: nativeTheme.shouldUseDarkColors, 15 | }, 16 | }); 17 | } 18 | 19 | export const nativeThemeMain: IpcMainBase = { 20 | ...ipcMainBase, 21 | 22 | initialize(mainWindow: BrowserWindow): void { 23 | ipcMainBase.initialize(mainWindow); 24 | ipcMainBase.handle('GET_NATIVE_THEME', async (): Promise => { 25 | return { 26 | shouldUseDarkColors: nativeTheme.shouldUseDarkColors, 27 | }; 28 | }); 29 | nativeTheme.on('updated', sendDataToHost); 30 | }, 31 | 32 | destroy() { 33 | nativeTheme.removeListener('updated', sendDataToHost); 34 | ipcMainBase.destroy(); 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /src/main/ipc-renderer/native-theme/renderer.ts: -------------------------------------------------------------------------------- 1 | import { getBaseIpcRenderer } from '../renderer-base'; 2 | import { Channel } from '../types'; 3 | 4 | import { NativeThemeInvokeData, NativeThemeToRendererData } from './types'; 5 | 6 | export const nativeThemeRenderer = getBaseIpcRenderer( 7 | Channel.NATIVE_THEME, 8 | ); 9 | -------------------------------------------------------------------------------- /src/main/ipc-renderer/native-theme/types.ts: -------------------------------------------------------------------------------- 1 | import { BaseEvent, BaseInvokeEvent } from '../types'; 2 | 3 | export interface NativeThemeData { 4 | shouldUseDarkColors: boolean; 5 | } 6 | 7 | export type NativeThemeToRendererData = BaseEvent<'NATIVE_THEME_UPDATED', NativeThemeData>; 8 | 9 | export type NativeThemeInvokeData = BaseInvokeEvent<'GET_NATIVE_THEME', void, NativeThemeData>; 10 | -------------------------------------------------------------------------------- /src/main/ipc-renderer/redis/renderer.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from 'data'; 2 | 3 | import { KeyData, PrefixesAndKeys, SshRedisAddress } from 'main/lib/redis'; 4 | 5 | import { getBaseIpcRenderer } from '../renderer-base'; 6 | import { Channel } from '../types'; 7 | 8 | import { IpcRendererRedis, RedisInvokeData } from './types'; 9 | 10 | const baseIpRenderer = getBaseIpcRenderer(Channel.REDIS); 11 | 12 | export const redisRenderer: IpcRendererRedis = { 13 | createRedis(data: Omit): Promise { 14 | return baseIpRenderer.invoke('CREATE_REDIS', data); 15 | }, 16 | deleteRedis(id: string): Promise { 17 | return baseIpRenderer.invoke('DELETE_REDIS', id); 18 | }, 19 | connectSsh(id: string): Promise<{ data?: Record; error?: string }> { 20 | return baseIpRenderer.invoke('CONNECT_SSH', id); 21 | }, 22 | disconnectSsh(id: string): Promise { 23 | return baseIpRenderer.invoke('DISCONNECT_SSH', id); 24 | }, 25 | connectRedis(data: { id: string; sshData: Record }): Promise<{ error?: string }> { 26 | return baseIpRenderer.invoke('CONNECT_REDIS', data); 27 | }, 28 | disconnect(id: string): Promise { 29 | return baseIpRenderer.invoke('DISCONNECT', id); 30 | }, 31 | getPrefixesAndKeys(data: { id: string; prefix: string[] }): Promise { 32 | return baseIpRenderer.invoke('GET_PREFIXES_AND_KEYS', data); 33 | }, 34 | getKeyData(data: { id: string; prefix: string[] }): Promise { 35 | return baseIpRenderer.invoke('GET_KEY_DATA', data); 36 | }, 37 | setKeyData(data: { id: string; data: KeyData }): Promise { 38 | return baseIpRenderer.invoke('SET_KEY_DATA', data); 39 | }, 40 | deleteKey(data: { id: string; key: string }): Promise { 41 | return baseIpRenderer.invoke('DELETE_KEY', data); 42 | }, 43 | setSshPassphrase(data: { id: string; passphrase: string }): Promise { 44 | return baseIpRenderer.invoke('SET_SSH_PASSPHRASE', data); 45 | }, 46 | setSshPassword(data: { id: string; password: string }): Promise { 47 | return baseIpRenderer.invoke('SET_SSH_PASSWORD', data); 48 | }, 49 | setTlsPassphrase(data: { id: string; passphrase: string }): Promise { 50 | return baseIpRenderer.invoke('SET_TLS_PASSPHRASE', data); 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /src/main/ipc-renderer/redis/types.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from 'data'; 2 | 3 | import { AskedRedisAuthData, KeyData, PrefixesAndKeys, SshRedisAddress } from 'main/lib/redis'; 4 | 5 | import { BaseInvokeEvent } from '../types'; 6 | 7 | export interface BaseRedis { 8 | connect(sshData: Record, data: AskedRedisAuthData): Promise; 9 | disconnect(): void; 10 | } 11 | 12 | export type RedisInvokeData = 13 | | BaseInvokeEvent<'CREATE_REDIS', Omit, string> 14 | | BaseInvokeEvent<'DELETE_REDIS', string, void> 15 | | BaseInvokeEvent<'CONNECT_SSH', string, { data?: Record; error?: string }> 16 | | BaseInvokeEvent<'DISCONNECT_SSH', string, void> 17 | | BaseInvokeEvent<'CONNECT_REDIS', { id: string; sshData: Record }, { error?: string }> 18 | | BaseInvokeEvent<'DISCONNECT', string, void> 19 | | BaseInvokeEvent<'GET_PREFIXES_AND_KEYS', { id: string; prefix: string[] }, PrefixesAndKeys> 20 | | BaseInvokeEvent<'GET_KEY_DATA', { id: string; prefix: string[] }, KeyData | undefined> 21 | | BaseInvokeEvent<'SET_KEY_DATA', { id: string; data: KeyData }, void> 22 | | BaseInvokeEvent<'DELETE_KEY', { id: string; key: string }, void> 23 | | BaseInvokeEvent<'SET_SSH_PASSPHRASE', { id: string; passphrase: string }, void> 24 | | BaseInvokeEvent<'SET_SSH_PASSWORD', { id: string; password: string }, void> 25 | | BaseInvokeEvent<'SET_TLS_PASSPHRASE', { id: string; passphrase: string }, void>; 26 | 27 | export interface IpcRendererRedis { 28 | createRedis(data: Omit): Promise; 29 | deleteRedis(id: string): Promise; 30 | connectSsh(id: string): Promise<{ data?: Record; error?: string }>; 31 | disconnectSsh(id: string): Promise; 32 | connectRedis(data: { id: string; sshData: Record }): Promise<{ error?: string }>; 33 | disconnect(id: string): Promise; 34 | getPrefixesAndKeys(data: { id: string; prefix: string[] }): Promise; 35 | getKeyData(data: { id: string; prefix: string[] }): Promise; 36 | setKeyData(data: { id: string; data: KeyData }): Promise; 37 | deleteKey(data: { id: string; key: string }): Promise; 38 | setSshPassphrase(data: { id: string; passphrase: string }): Promise; 39 | setSshPassword(data: { id: string; password: string }): Promise; 40 | setTlsPassphrase(data: { id: string; passphrase: string }): Promise; 41 | } 42 | -------------------------------------------------------------------------------- /src/main/ipc-renderer/renderer-base.ts: -------------------------------------------------------------------------------- 1 | import { IpcRendererEvent, ipcRenderer } from 'electron'; 2 | 3 | import { 4 | BaseEvent, 5 | BaseInvokeEvent, 6 | EventData, 7 | EventHandler, 8 | EventResponse, 9 | EventType, 10 | IpcRendererBase, 11 | UnsubscribeFn, 12 | } from './types'; 13 | 14 | export function getBaseIpcRenderer< 15 | TToRenderer extends BaseEvent, 16 | TFromRenderer extends BaseEvent, 17 | TInvokeFromRendererData extends BaseInvokeEvent, 18 | >(channel: string): IpcRendererBase { 19 | return { 20 | sendMessage(data: TFromRenderer): void { 21 | ipcRenderer.send(channel, data); 22 | }, 23 | 24 | on>( 25 | eventType: Type, 26 | func: EventHandler>, 27 | ): UnsubscribeFn { 28 | const subscription = (_event: IpcRendererEvent, data: TToRenderer): void => { 29 | if (data.type === eventType) { 30 | func(data.data); 31 | } 32 | }; 33 | ipcRenderer.on(channel, subscription); 34 | 35 | return () => { 36 | ipcRenderer.removeListener(channel, subscription); 37 | }; 38 | }, 39 | 40 | once>(eventType: Type, func: EventHandler>): void { 41 | const subscription = (_event: IpcRendererEvent, data: TToRenderer): void => { 42 | if (data.type === eventType) { 43 | func(data.data); 44 | } 45 | }; 46 | ipcRenderer.once(channel, subscription); 47 | }, 48 | 49 | invoke>( 50 | eventType: Type, 51 | data: EventData, 52 | ): Promise> { 53 | return ipcRenderer.invoke(channel, { 54 | type: eventType, 55 | data, 56 | }); 57 | }, 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /src/main/ipc-renderer/renderer.ts: -------------------------------------------------------------------------------- 1 | export * from './native-theme/renderer'; 2 | export * from './menu/renderer'; 3 | export * from './redis/renderer'; 4 | -------------------------------------------------------------------------------- /src/main/ipc-renderer/types.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow } from 'electron'; 2 | 3 | export enum Channel { 4 | NATIVE_THEME = 'native-theme', 5 | MENU = 'menu', 6 | REDIS = 'redis', 7 | } 8 | 9 | export type EventHandler = (value: T) => void; 10 | 11 | export type UnsubscribeFn = () => void; 12 | 13 | export interface BaseEvent { 14 | type: T; 15 | data: D; 16 | } 17 | 18 | export interface BaseInvokeEvent { 19 | type: T; 20 | data: D; 21 | response: R; 22 | } 23 | 24 | export type EventType = T extends { type: string } ? T['type'] : never; 25 | 26 | export type EventData = T extends { type: Type; data: any } ? T['data'] : never; 27 | 28 | export type EventAnyData = T extends { data: any } ? T['data'] : never; 29 | 30 | export type EventResponse = T extends { type: Type; response: any } ? T['response'] : never; 31 | 32 | export type EventAnyResponse = T extends { response: any } ? T['response'] : never; 33 | 34 | export type InvokeListener = ( 35 | data: EventData, 36 | ) => Promise>; 37 | 38 | export interface IpcRendererBase< 39 | TToRenderer extends BaseEvent, 40 | TFromRenderer extends BaseEvent, 41 | TInvokeFromRendererData extends BaseInvokeEvent, 42 | > { 43 | sendMessage(data: TFromRenderer): void; 44 | on>( 45 | eventType: Type, 46 | func: EventHandler>, 47 | ): UnsubscribeFn; 48 | once>(eventType: Type, func: EventHandler>): void; 49 | invoke>( 50 | eventType: Type, 51 | data: EventData, 52 | ): Promise>; 53 | } 54 | 55 | export interface IpcMainBase< 56 | TToRenderer extends BaseEvent, 57 | TFromRenderer extends BaseEvent, 58 | TInvokeFromRendererData extends BaseInvokeEvent, 59 | > { 60 | initialize(mainWindow: BrowserWindow): void; 61 | destroy(): void; 62 | on>( 63 | eventType: Type, 64 | func: EventHandler>, 65 | ): UnsubscribeFn; 66 | once>( 67 | eventType: Type, 68 | func: EventHandler>, 69 | ): void; 70 | sendMessage(data: TToRenderer): void; 71 | handle>( 72 | eventType: Type, 73 | handler: InvokeListener, 74 | ): void; 75 | } 76 | -------------------------------------------------------------------------------- /src/main/lib/redis/index.ts: -------------------------------------------------------------------------------- 1 | export { Redis } from './redis'; 2 | export type { KeyData, SshRedisAddress, PrefixesAndKeys, AskedRedisAuthData } from './types'; 3 | -------------------------------------------------------------------------------- /src/main/lib/redis/tunnel-ssh.ts: -------------------------------------------------------------------------------- 1 | import { AddressInfo, Server } from 'net'; 2 | import { Client, ConnectConfig } from 'ssh2'; 3 | 4 | import { ConnectionMain } from 'data'; 5 | 6 | import { createTunnel } from '../tunnel-ssh'; 7 | 8 | import { AskedSshAuthData, SshRedisAddress } from './types'; 9 | 10 | const LOCAL_HOST = '127.0.0.1'; 11 | 12 | // Initial port for creating ssh server 13 | let PORT = 40000; 14 | 15 | export class TunnelSsh { 16 | private _sshServer?: Server; 17 | private _sshClient?: Client; 18 | private _config?: ConnectConfig; 19 | 20 | constructor(config?: ConnectConfig) { 21 | this._config = config; 22 | } 23 | 24 | private _sshConnect(data: AskedSshAuthData, host?: string, port?: number): Promise { 25 | if (!this._config || !host || !port) { 26 | return Promise.resolve(undefined); 27 | } 28 | 29 | const config: ConnectConfig = { 30 | ...this._config, 31 | password: this._config.password || data.password, 32 | passphrase: this._config.passphrase || data.passphrase, 33 | }; 34 | 35 | return createTunnel({ autoClose: true }, { port: PORT++ }, config, { 36 | srcAddr: LOCAL_HOST, 37 | srcPort: port, 38 | dstAddr: host, 39 | dstPort: port, 40 | }).then(([server, client]) => { 41 | this._sshServer = server; 42 | this._sshClient = client; 43 | 44 | return { 45 | originalHost: host, 46 | originalPort: port, 47 | host: LOCAL_HOST, 48 | port: (server.address() as AddressInfo).port, 49 | }; 50 | }); 51 | } 52 | 53 | async connect(main: ConnectionMain, data: AskedSshAuthData): Promise> { 54 | const result: Record = {}; 55 | 56 | const connectResult = await Promise.all( 57 | main.addresses.map(({ host, port }) => this._sshConnect(data, host, port ? Number(port) : undefined)), 58 | ); 59 | 60 | connectResult.forEach((resData) => { 61 | if (resData) { 62 | result[`${resData.host}:${resData.port}`] = resData; 63 | } 64 | }); 65 | 66 | return result; 67 | } 68 | 69 | async disconnect(): Promise { 70 | return new Promise((resolve, reject) => { 71 | this._sshClient?.end(); 72 | 73 | if (!this._sshServer) { 74 | resolve(); 75 | return; 76 | } 77 | 78 | this._sshServer.close((err) => { 79 | if (err) { 80 | reject(err); 81 | return; 82 | } 83 | 84 | resolve(); 85 | }); 86 | this._sshServer = undefined; 87 | this._sshClient = undefined; 88 | }); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/lib/redis/types.ts: -------------------------------------------------------------------------------- 1 | export interface AskedSshAuthData { 2 | passphrase?: string; 3 | password?: string; 4 | } 5 | 6 | export interface AskedRedisAuthData { 7 | tlsPassphrase?: string; 8 | } 9 | 10 | export interface PrefixesAndKeys { 11 | prefixes: Record; 12 | keys: string[]; 13 | } 14 | 15 | export interface KeyData { 16 | key: string; 17 | ttl?: number; 18 | value: string; 19 | } 20 | 21 | export interface SshRedisAddress { 22 | originalHost: string; 23 | originalPort: number; 24 | host: string; 25 | port: number; 26 | } 27 | -------------------------------------------------------------------------------- /src/main/lib/tunnel-ssh/index.ts: -------------------------------------------------------------------------------- 1 | export { createTunnel } from './tunnel-ssh'; 2 | export type { ForwardOptions, TunnelOptions } from './types'; 3 | -------------------------------------------------------------------------------- /src/main/lib/tunnel-ssh/tunnel-ssh.ts: -------------------------------------------------------------------------------- 1 | import net, { ListenOptions, Server, Socket } from 'net'; 2 | import { Client, ConnectConfig } from 'ssh2'; 3 | 4 | import { ForwardOptions, TunnelOptions } from './types'; 5 | 6 | function autoClose(server: Server, connection: Socket): void { 7 | connection.on('close', () => { 8 | server.getConnections((error, count) => { 9 | if (count === 0) { 10 | server.close(); 11 | } 12 | }); 13 | }); 14 | } 15 | 16 | async function createServer(options: ListenOptions): Promise { 17 | return new Promise((resolve, reject) => { 18 | const server = net.createServer(); 19 | const errorHandler = (error: Error): void => { 20 | reject(error); 21 | }; 22 | server.on('error', errorHandler); 23 | process.on('uncaughtException', errorHandler); 24 | server.listen(options); 25 | server.on('listening', () => { 26 | process.removeListener('uncaughtException' as any, errorHandler); 27 | resolve(server); 28 | }); 29 | }); 30 | } 31 | 32 | async function createClient(config: ConnectConfig): Promise { 33 | return new Promise(function (resolve, reject) { 34 | const conn = new Client(); 35 | conn.on('ready', () => resolve(conn)); 36 | conn.on('error', reject); 37 | conn.connect(config); 38 | }); 39 | } 40 | 41 | export async function createTunnel( 42 | tunnelOptions: TunnelOptions, 43 | serverOptions: ListenOptions, 44 | sshOptions: ConnectConfig, 45 | forwardOptions: ForwardOptions, 46 | ): Promise<[Server, Client]> { 47 | return new Promise(async (resolve, reject) => { 48 | let server: Server; 49 | let conn: Client; 50 | try { 51 | server = await createServer(serverOptions); 52 | } catch (e) { 53 | return reject(e); 54 | } 55 | 56 | try { 57 | conn = await createClient(sshOptions); 58 | } catch (e) { 59 | server.close(); 60 | return reject(e); 61 | } 62 | server.on('connection', (connection) => { 63 | if (tunnelOptions.autoClose) { 64 | autoClose(server, connection); 65 | } 66 | 67 | conn.forwardOut( 68 | forwardOptions.srcAddr, 69 | forwardOptions.srcPort, 70 | forwardOptions.dstAddr, 71 | forwardOptions.dstPort, 72 | (err, stream) => { 73 | connection.pipe(stream).pipe(connection); 74 | }, 75 | ); 76 | }); 77 | 78 | server.on('close', () => conn.end()); 79 | resolve([server, conn]); 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /src/main/lib/tunnel-ssh/types.ts: -------------------------------------------------------------------------------- 1 | export interface TunnelOptions { 2 | autoClose?: boolean; 3 | } 4 | 5 | export interface ForwardOptions { 6 | srcAddr: string; 7 | srcPort: number; 8 | dstAddr: string; 9 | dstPort: number; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/preload.ts: -------------------------------------------------------------------------------- 1 | import { menuRenderer, nativeThemeRenderer, redisRenderer } from './ipc-renderer/renderer'; 2 | import { contextBridge } from 'electron'; 3 | 4 | const electronHandler = { 5 | nativeTheme: nativeThemeRenderer, 6 | menu: menuRenderer, 7 | redis: redisRenderer, 8 | }; 9 | 10 | contextBridge.exposeInMainWorld('electron', electronHandler); 11 | 12 | export type ElectronHandler = typeof electronHandler; 13 | -------------------------------------------------------------------------------- /src/main/util.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { URL } from 'url'; 3 | 4 | export function resolveHtmlPath(htmlFileName: string): string { 5 | if (process.env.NODE_ENV === 'development') { 6 | const port = process.env.PORT || 1212; 7 | const url = new URL(`http://localhost:${port}`); 8 | url.pathname = htmlFileName; 9 | return url.href; 10 | } 11 | return `file://${path.resolve(__dirname, '../renderer/', htmlFileName)}`; 12 | } 13 | -------------------------------------------------------------------------------- /src/renderer/@types/preload.d.ts: -------------------------------------------------------------------------------- 1 | import { ElectronHandler } from 'main/preload'; 2 | 3 | declare global { 4 | interface Window { 5 | electron: ElectronHandler; 6 | } 7 | } 8 | 9 | export {}; 10 | -------------------------------------------------------------------------------- /src/renderer/@types/styles.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.pcss' { 2 | const classes: { [key: string]: string }; 3 | export default classes; 4 | } 5 | -------------------------------------------------------------------------------- /src/renderer/@types/tunnel-ssh.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'tunnel-ssh' { 2 | import { ListenOptions, Server } from 'net'; 3 | import { ConnectConfig, Client } from 'ssh2'; 4 | 5 | export function createTunnel( 6 | tunnelOptions: createTunnel.TunnelOptions, 7 | serverOptions: ListenOptions, 8 | sshOptions: ConnectConfig, 9 | forwardOptions: createTunnel.ForwardOptions, 10 | ): Promise<[Server, Client]>; 11 | 12 | declare namespace createTunnel { 13 | interface TunnelOptions { 14 | autoClose?: boolean; 15 | } 16 | 17 | interface ForwardOptions { 18 | srcAddr: string; 19 | srcPort: number; 20 | dstAddr: string; 21 | dstPort: number; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/renderer/components/ask-data-form/ask-data-form.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .ask-data-form { 4 | &__item { 5 | &:not(:last-of-type) { 6 | margin-bottom: var(--grid2); 7 | } 8 | } 9 | 10 | &__actions { 11 | display: flex; 12 | justify-content: flex-end; 13 | margin-top: var(--grid4); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/renderer/components/ask-data-form/index.ts: -------------------------------------------------------------------------------- 1 | export { AskDataForm } from './ask-data-form'; 2 | export type { AskDataValues } from './types'; 3 | -------------------------------------------------------------------------------- /src/renderer/components/ask-data-form/types.ts: -------------------------------------------------------------------------------- 1 | export enum AskDataField { 2 | SshPassphrase = 'sshPassphrase', 3 | SshPassword = 'sshPassword', 4 | TlsPassphrase = 'tlsPassphrase', 5 | } 6 | 7 | export interface AskDataValues { 8 | [AskDataField.SshPassphrase]?: string; 9 | [AskDataField.SshPassword]?: string; 10 | [AskDataField.TlsPassphrase]?: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/renderer/constants/app-constants.ts: -------------------------------------------------------------------------------- 1 | export const DB_NAME = 'redis-gui-store'; 2 | export const DB_VERSION = 1; 3 | export const DB_CONNECTIONS_STORE = 'connections'; 4 | -------------------------------------------------------------------------------- /src/renderer/data/connections.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | 3 | import { Connection } from 'data'; 4 | 5 | import { dbPromise } from 'renderer/lib/db'; 6 | 7 | import { DB_CONNECTIONS_STORE } from 'renderer/constants/app-constants'; 8 | 9 | export async function list(): Promise { 10 | const db = await dbPromise; 11 | const connections = await db.getAll(DB_CONNECTIONS_STORE); 12 | return connections; 13 | } 14 | 15 | export async function create(data: Omit): Promise { 16 | const db = await dbPromise; 17 | 18 | const connectionData = { 19 | id: uuidv4(), 20 | ...data, 21 | }; 22 | 23 | await db.add(DB_CONNECTIONS_STORE, connectionData as Connection); 24 | 25 | return connectionData; 26 | } 27 | 28 | export async function update(id: string, data: Omit): Promise { 29 | const db = await dbPromise; 30 | 31 | const connectionData = { ...data, id }; 32 | await db.put(DB_CONNECTIONS_STORE, connectionData as Connection); 33 | 34 | return connectionData; 35 | } 36 | 37 | export async function deleteItem(id: string): Promise { 38 | const db = await dbPromise; 39 | await db.delete(DB_CONNECTIONS_STORE, id); 40 | } 41 | -------------------------------------------------------------------------------- /src/renderer/data/index.ts: -------------------------------------------------------------------------------- 1 | export * as connectionsClient from './connections'; 2 | -------------------------------------------------------------------------------- /src/renderer/hooks/id-hook.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | 4 | export function useIdHook(initialId?: string): string | undefined { 5 | const [id, setId] = useState(initialId); 6 | 7 | useEffect(() => { 8 | if (!id) { 9 | setId(uuidv4()); 10 | } 11 | }, [id]); 12 | 13 | return id; 14 | } 15 | -------------------------------------------------------------------------------- /src/renderer/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useIdHook } from './id-hook'; 2 | -------------------------------------------------------------------------------- /src/renderer/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | Redis GUI 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/renderer/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; 2 | 3 | import { App } from 'renderer/scenes/app'; 4 | 5 | const container = document.getElementById('root'); 6 | if (container) { 7 | const root = createRoot(container); 8 | root.render(); 9 | } 10 | -------------------------------------------------------------------------------- /src/renderer/lib/bem/bem.ts: -------------------------------------------------------------------------------- 1 | import { ClassNameGenerator, State } from './types'; 2 | 3 | const hyphenRegExp = /-([a-z])/g; 4 | 5 | function toCamelCase(value: string): string { 6 | return value.replace(hyphenRegExp, (g) => g[1].toUpperCase()); 7 | } 8 | 9 | function getModifierClasses(element: string, state: State): string[] { 10 | const result: string[] = []; 11 | 12 | Object.keys(state).forEach((key) => { 13 | if (state[key] === true) { 14 | result.push(`${toCamelCase(element)}_${toCamelCase(key)}`); 15 | } else if (state[key]) { 16 | const camelCaseKey = toCamelCase(key); 17 | result.push(`${toCamelCase(element)}_${camelCaseKey}_${state[camelCaseKey]}`); 18 | } 19 | }); 20 | 21 | return result; 22 | } 23 | 24 | function getBlockClasses(block: string, theme?: string, additionalClassName?: string): string[] { 25 | const result: string[] = []; 26 | 27 | const themeStyleClass = theme ? `${toCamelCase(block)}_theme_${theme}` : ''; 28 | result.push(toCamelCase(block)); 29 | 30 | if (themeStyleClass) { 31 | result.push(themeStyleClass); 32 | } 33 | 34 | if (additionalClassName) { 35 | result.push(additionalClassName); 36 | } 37 | 38 | return result; 39 | } 40 | 41 | function getElementClass(block: string, element: string): string { 42 | return toCamelCase(`${block}__${element}`); 43 | } 44 | 45 | /** 46 | * Module for generating class names. 47 | */ 48 | export function block( 49 | styles: Record, 50 | blockName: string, 51 | theme?: string, 52 | additionalClassName?: string, 53 | ): ClassNameGenerator { 54 | return (elementNameOrState?: string | State, state?: State): string => { 55 | const result: string[] = []; 56 | let element = blockName; 57 | 58 | if (elementNameOrState) { 59 | if (typeof elementNameOrState === 'string') { 60 | // element 61 | element = getElementClass(blockName, elementNameOrState); 62 | result.push(element); 63 | } else if (typeof elementNameOrState === 'object') { 64 | // block with modifiers 65 | result.push(...getBlockClasses(blockName, theme, additionalClassName)); 66 | result.push(...getModifierClasses(blockName, elementNameOrState)); 67 | } 68 | } else { 69 | // block 70 | result.push(...getBlockClasses(blockName, theme, additionalClassName)); 71 | } 72 | 73 | if (state) { 74 | // modifiers for element 75 | result.push(...getModifierClasses(element, state)); 76 | } 77 | 78 | return result 79 | .map((className) => (className === additionalClassName ? className : styles[className])) 80 | .filter(Boolean) 81 | .join(' '); 82 | }; 83 | } 84 | -------------------------------------------------------------------------------- /src/renderer/lib/bem/index.ts: -------------------------------------------------------------------------------- 1 | export type { ClassNameGenerator } from './types'; 2 | export { block } from './bem'; 3 | -------------------------------------------------------------------------------- /src/renderer/lib/bem/types.ts: -------------------------------------------------------------------------------- 1 | export type State = Record; 2 | 3 | export interface ClassNameGenerator { 4 | (elementName?: string, state?: State): string; 5 | (state?: State): string; 6 | } 7 | -------------------------------------------------------------------------------- /src/renderer/lib/db/db.ts: -------------------------------------------------------------------------------- 1 | import { openDB } from 'idb'; 2 | 3 | import { DB_CONNECTIONS_STORE, DB_NAME, DB_VERSION } from 'renderer/constants/app-constants'; 4 | 5 | import { Db } from './types'; 6 | 7 | export const dbPromise = openDB(DB_NAME, DB_VERSION, { 8 | upgrade(db) { 9 | db.createObjectStore(DB_CONNECTIONS_STORE, { keyPath: 'id' }); 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /src/renderer/lib/db/index.ts: -------------------------------------------------------------------------------- 1 | export { dbPromise } from './db'; 2 | -------------------------------------------------------------------------------- /src/renderer/lib/db/types.ts: -------------------------------------------------------------------------------- 1 | import { DBSchema } from 'idb'; 2 | 3 | import type { Connection } from 'data'; 4 | 5 | export interface Db extends DBSchema { 6 | connections: { 7 | value: Connection; 8 | key: string; 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/renderer/lib/keyboard.ts: -------------------------------------------------------------------------------- 1 | import { KeyboardEvent } from 'react'; 2 | 3 | enum Key { 4 | Enter = 'Enter', 5 | } 6 | 7 | export function isEnterEvent(event: KeyboardEvent): boolean { 8 | return event.key === Key.Enter; 9 | } 10 | 11 | export function handleEnterEvent(callback: (event: KeyboardEvent) => void) { 12 | return (event: KeyboardEvent): void => { 13 | if (isEnterEvent(event)) { 14 | callback(event); 15 | } 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/renderer/lib/media/index.ts: -------------------------------------------------------------------------------- 1 | export { Media } from './types'; 2 | export { mediaQueries } from './mq'; 3 | export { getMatchMedia, releaseMatchMedia } from './utils'; 4 | -------------------------------------------------------------------------------- /src/renderer/lib/media/mq.ts: -------------------------------------------------------------------------------- 1 | import mq from '../../mq.json'; 2 | 3 | import { Media } from './types'; 4 | 5 | export const mediaQueries: Record = mq; 6 | -------------------------------------------------------------------------------- /src/renderer/lib/media/types.ts: -------------------------------------------------------------------------------- 1 | export enum Media { 2 | MobileS = '--mobile-s', 3 | MobileM = '--mobile-m', 4 | MobileL = '--mobile-l', 5 | Mobile = '--mobile', 6 | TabletS = '--tablet-s', 7 | TabletM = '--tablet-m', 8 | Tablet = '--tablet', 9 | DesktopS = '--desktop-s', 10 | DesktopM = '--desktop-m', 11 | DesktopL = '--desktop-l', 12 | DesktopXL = '--desktop-xl', 13 | Desktop = '--desktop', 14 | } 15 | -------------------------------------------------------------------------------- /src/renderer/lib/media/use-mq.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | import { Media } from './types'; 4 | import { getMatchMedia, releaseMatchMedia } from './utils'; 5 | 6 | const IS_BROWSER = window !== undefined; 7 | const SUPPORTS_TOUCH = (IS_BROWSER && 'ontouchstart' in window) || navigator.maxTouchPoints > 0; 8 | 9 | export function useMq(media: Media | string, touch?: boolean): boolean { 10 | const [isMatched, setIsMatched] = useState(false); 11 | 12 | function setMatchesValue(matches: boolean): void { 13 | let queryPass = true; 14 | let touchPass = true; 15 | 16 | if (media) { 17 | queryPass = matches; 18 | } 19 | if (touch) { 20 | touchPass = SUPPORTS_TOUCH; 21 | } else if (touch === false) { 22 | touchPass = !SUPPORTS_TOUCH; 23 | } 24 | 25 | const result = queryPass && touchPass; 26 | if (result !== isMatched) { 27 | setIsMatched(result); 28 | } 29 | } 30 | 31 | function handleMatch(event: MediaQueryListEvent): void { 32 | setMatchesValue(event.matches); 33 | } 34 | 35 | useEffect(() => { 36 | const mql = getMatchMedia(media); 37 | mql.addEventListener('change', handleMatch); 38 | setMatchesValue(mql.matches); 39 | 40 | return () => { 41 | releaseMatchMedia(media); 42 | mql.removeEventListener('change', handleMatch); 43 | }; 44 | }); 45 | 46 | return isMatched; 47 | } 48 | -------------------------------------------------------------------------------- /src/renderer/lib/media/utils.ts: -------------------------------------------------------------------------------- 1 | import { mediaQueries } from './mq'; 2 | import { Media } from './types'; 3 | 4 | const pool: Record = {}; 5 | const refCounters: Record = {}; 6 | 7 | export function getMatchMedia(queryProp: Media | string): MediaQueryList { 8 | const query = mediaQueries[queryProp as Media] || queryProp; 9 | 10 | if (!pool[query]) { 11 | pool[query] = window.matchMedia(query); 12 | refCounters[query] = 1; 13 | } else { 14 | refCounters[query] += 1; 15 | } 16 | 17 | return pool[query]; 18 | } 19 | 20 | export function releaseMatchMedia(queryProp: Media | string): void { 21 | const query = mediaQueries[queryProp as Media] || queryProp; 22 | 23 | refCounters[query] -= 1; 24 | 25 | if (pool[query] && refCounters[query] === 0) { 26 | delete pool[query]; 27 | delete refCounters[query]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/renderer/lib/mobx.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, ReactElement, createContext, useContext, useMemo } from 'react'; 2 | 3 | type UseStoreFn = () => Store; 4 | 5 | type HocType = (component: FC) => FC; 6 | 7 | export function glueStore(store: Store): [UseStoreFn, HocType] { 8 | const StoreContext = createContext(store); 9 | const StoreProvider = StoreContext.Provider; 10 | 11 | const useStore = (): Store => useContext(StoreContext); 12 | const useNewStore = (): Store => useMemo(() => store, []); 13 | 14 | function hoc(component: FC) { 15 | return function HocComponent(props: OwnProps): ReactElement { 16 | const componentStore = useNewStore(); 17 | 18 | return {React.createElement(component, props)}; 19 | }; 20 | } 21 | 22 | return [useStore, hoc]; 23 | } 24 | -------------------------------------------------------------------------------- /src/renderer/lib/numbers.ts: -------------------------------------------------------------------------------- 1 | const notNumberSymbolRegExp = /[^\d,-]/g; 2 | const commaRegExp = /,/g; 3 | 4 | export function parseNumber(str: string): number { 5 | return Number(str.replace(notNumberSymbolRegExp, '').replace(commaRegExp, '.')); 6 | } 7 | -------------------------------------------------------------------------------- /src/renderer/lib/page.ts: -------------------------------------------------------------------------------- 1 | import { AnyError, PageState } from 'renderer/types'; 2 | 3 | interface StateData { 4 | loadingKeys: boolean[]; 5 | errorKeys?: (AnyError | null)[]; 6 | readyData?: any[]; // eslint-disable-line @typescript-eslint/no-explicit-any 7 | } 8 | 9 | export function calculatePageState({ loadingKeys, errorKeys, readyData }: StateData): PageState { 10 | if (loadingKeys.some(Boolean)) { 11 | return PageState.LOADING; 12 | } 13 | 14 | if (errorKeys?.some((error) => error !== null)) { 15 | return PageState.ERROR; 16 | } 17 | 18 | if (readyData === undefined || readyData.every((data) => data !== undefined && data !== null)) { 19 | return PageState.READY; 20 | } 21 | 22 | return PageState.ERROR; 23 | } 24 | -------------------------------------------------------------------------------- /src/renderer/lib/theme/context.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-function */ 2 | import { createContext, useContext } from 'react'; 3 | 4 | import { Theme } from './types'; 5 | 6 | type ThemeData = [Theme, (theme: Theme) => void]; 7 | 8 | export const ThemeContext = createContext([Theme.Light, () => {}]); 9 | 10 | export const useTheme = (): ThemeData => useContext(ThemeContext); 11 | -------------------------------------------------------------------------------- /src/renderer/lib/theme/index.ts: -------------------------------------------------------------------------------- 1 | export { ThemeContextProvider } from './provider'; 2 | export { ThemeContext, useTheme } from './context'; 3 | export { Theme } from './types'; 4 | export { useStyles } from './styles'; 5 | -------------------------------------------------------------------------------- /src/renderer/lib/theme/provider.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement, ReactNode, useState } from 'react'; 2 | 3 | import { ThemeContext } from './context'; 4 | import { Theme } from './types'; 5 | 6 | interface Props { 7 | children: ReactNode; 8 | systemTheme: Theme; 9 | useSystemTheme?: boolean; 10 | } 11 | 12 | const THEME_ITEM = 'theme'; 13 | const themesList = Object.values(Theme) as string[]; 14 | 15 | export function ThemeContextProvider({ children, systemTheme, useSystemTheme }: Props): ReactElement { 16 | function setTheme(type: Theme): void { 17 | window.localStorage?.setItem(THEME_ITEM, type); 18 | setStateTheme(type); 19 | } 20 | 21 | const storageTheme = window.localStorage?.getItem(THEME_ITEM); 22 | const initialTheme = 23 | systemTheme || (storageTheme && themesList.includes(storageTheme) ? (storageTheme as Theme) : Theme.Light); 24 | 25 | const [theme, setStateTheme] = useState(initialTheme); 26 | 27 | return ( 28 | {children} 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/renderer/lib/theme/styles.ts: -------------------------------------------------------------------------------- 1 | import { ClassNameGenerator, block } from 'renderer/lib/bem'; 2 | 3 | import { useTheme } from './context'; 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | export function useStyles>( 7 | styles: S, 8 | className: string, 9 | additionalClassName?: string, 10 | ): ClassNameGenerator { 11 | const [theme] = useTheme(); 12 | 13 | return block(styles, className, theme, additionalClassName); 14 | } 15 | -------------------------------------------------------------------------------- /src/renderer/lib/theme/types.ts: -------------------------------------------------------------------------------- 1 | export enum Theme { 2 | Light = 'light', 3 | Dark = 'dark', 4 | } 5 | -------------------------------------------------------------------------------- /src/renderer/lib/yup.ts: -------------------------------------------------------------------------------- 1 | /* Here is the place where you can extend yup */ 2 | import * as yup from 'yup'; 3 | 4 | import { validationTexts } from 'texts'; 5 | 6 | yup.setLocale({ 7 | mixed: { 8 | required: validationTexts.mixedRequired, 9 | default: validationTexts.mixedDefault, 10 | }, 11 | string: { 12 | email: validationTexts.stringEmail, 13 | url: validationTexts.stringUrl, 14 | }, 15 | }); 16 | 17 | export { yup }; 18 | -------------------------------------------------------------------------------- /src/renderer/mq.json: -------------------------------------------------------------------------------- 1 | { 2 | "--mobile-s": "(min-width: 320px)", 3 | "--mobile-m": "(min-width: 375px)", 4 | "--mobile-l": "(min-width: 412px)", 5 | "--mobile": "(max-width: 599px)", 6 | "--tablet-s": "(min-width: 600px)", 7 | "--tablet-m": "(min-width: 768px)", 8 | "--tablet": "(min-width: 600px) and (max-width: 1023px)", 9 | "--desktop-s": "(min-width: 1024px)", 10 | "--desktop-m": "(min-width: 1280px)", 11 | "--desktop-l": "(min-width: 1440px)", 12 | "--desktop-xl": "(min-width: 1920px)", 13 | "--desktop": "(min-width: 1024px)" 14 | } 15 | -------------------------------------------------------------------------------- /src/renderer/scenes/app/app.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement, useEffect, useState } from 'react'; 2 | 3 | import { NativeThemeData } from 'main/ipc-renderer/native-theme/types'; 4 | 5 | import { Theme, ThemeContextProvider } from 'renderer/lib/theme'; 6 | 7 | import { MainPage } from 'renderer/scenes/main-page'; 8 | 9 | import 'renderer/styles/reset.pcss'; 10 | 11 | export function App(): ReactElement { 12 | const [shouldUseDarkColors, setShouldUseDarkColors] = useState(false); 13 | 14 | useEffect(() => { 15 | getNativeThemeData(); 16 | }, []); 17 | 18 | useEffect(() => { 19 | const unsubscribe = window.electron.nativeTheme.on('NATIVE_THEME_UPDATED', handleThemeChange); 20 | 21 | return () => { 22 | unsubscribe(); 23 | }; 24 | }, []); 25 | 26 | async function getNativeThemeData(): Promise { 27 | const { shouldUseDarkColors } = await window.electron.nativeTheme.invoke('GET_NATIVE_THEME', undefined); 28 | setShouldUseDarkColors(shouldUseDarkColors); 29 | } 30 | 31 | function handleThemeChange({ shouldUseDarkColors }: NativeThemeData): void { 32 | setShouldUseDarkColors(shouldUseDarkColors); 33 | } 34 | 35 | return ( 36 | 37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/renderer/scenes/app/index.ts: -------------------------------------------------------------------------------- 1 | export { App } from './app'; 2 | -------------------------------------------------------------------------------- /src/renderer/scenes/connection-modal/components/advanced-form/advanced-form.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .advanced-form { 4 | &__item { 5 | &:not(:last-of-type) { 6 | margin-bottom: var(--grid2); 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/renderer/scenes/connection-modal/components/advanced-form/advanced-form.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | 3 | import { useStyles } from 'renderer/lib/theme'; 4 | 5 | import { Checkbox, CheckboxSize, CheckboxWidth } from 'renderer/ui/checkbox'; 6 | import { FormikField } from 'renderer/ui/formik-field'; 7 | import { Input, InputSize, InputWidth } from 'renderer/ui/input'; 8 | import { NumberInput } from 'renderer/ui/number-input'; 9 | 10 | import { ConnectionAdvancedFormikField, ConnectionFormikField } from 'renderer/scenes/connection-modal/types'; 11 | 12 | import { advancedFormTexts } from 'texts'; 13 | 14 | import styles from './advanced-form.pcss'; 15 | 16 | interface Props { 17 | isSaving: boolean; 18 | } 19 | 20 | function getFieldName(field: ConnectionAdvancedFormikField): string { 21 | return `${ConnectionFormikField.Advanced}.${field}`; 22 | } 23 | 24 | export function AdvancedForm({ isSaving }: Props): ReactElement { 25 | const cn = useStyles(styles, 'advanced-form'); 26 | 27 | return ( 28 |
29 | 41 | 42 | 54 | 55 | 66 | 67 | 78 |
79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /src/renderer/scenes/connection-modal/components/advanced-form/index.ts: -------------------------------------------------------------------------------- 1 | export { AdvancedForm } from './advanced-form'; 2 | -------------------------------------------------------------------------------- /src/renderer/scenes/connection-modal/components/authentication-form/authentication-form.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .authentication-form { 4 | &__item { 5 | &:not(:last-of-type) { 6 | margin-bottom: var(--grid2); 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/renderer/scenes/connection-modal/components/authentication-form/index.ts: -------------------------------------------------------------------------------- 1 | export { AuthenticationForm } from './authentication-form'; 2 | -------------------------------------------------------------------------------- /src/renderer/scenes/connection-modal/components/main-form/index.ts: -------------------------------------------------------------------------------- 1 | export { MainForm, useMainField } from './main-form'; 2 | -------------------------------------------------------------------------------- /src/renderer/scenes/connection-modal/components/main-form/main-form.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .connection-tab-form { 4 | &__item { 5 | &:not(:last-of-type) { 6 | margin-bottom: var(--grid2); 7 | } 8 | } 9 | 10 | &__address { 11 | display: flex; 12 | align-items: center; 13 | 14 | &:not(:last-of-type) { 15 | margin-bottom: var(--grid2); 16 | } 17 | } 18 | 19 | &__host { 20 | flex: 2; 21 | margin-right: var(--grid2); 22 | } 23 | 24 | &__port { 25 | flex: 1; 26 | } 27 | 28 | &__remove-btn { 29 | flex: 0; 30 | margin-top: var(--grid5); 31 | margin-left: var(--grid2); 32 | } 33 | 34 | &__add-btn-wrap { 35 | display: flex; 36 | justify-content: flex-end; 37 | margin-top: var(--grid2); 38 | } 39 | 40 | &__addresses-label { 41 | margin-top: var(--grid4); 42 | margin-bottom: var(--grid2); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/renderer/scenes/connection-modal/components/ssh-form/index.ts: -------------------------------------------------------------------------------- 1 | export { SshForm } from './ssh-form'; 2 | -------------------------------------------------------------------------------- /src/renderer/scenes/connection-modal/components/ssh-form/ssh-form.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .ssh-form { 4 | &__item { 5 | &:not(:last-of-type) { 6 | margin-bottom: var(--grid2); 7 | } 8 | } 9 | 10 | &__address-item { 11 | display: flex; 12 | } 13 | 14 | &__host { 15 | flex: 1; 16 | } 17 | 18 | &__port { 19 | margin-left: var(--grid2); 20 | flex: 0 0 60px; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/scenes/connection-modal/components/test-connect-result/index.ts: -------------------------------------------------------------------------------- 1 | export { TestConnectResult } from './test-connect-result'; 2 | -------------------------------------------------------------------------------- /src/renderer/scenes/connection-modal/components/test-connect-result/test-connect-result.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .test-connect-result { 4 | &__content { 5 | margin: var(--grid2) 0; 6 | } 7 | 8 | &__message { 9 | display: flex; 10 | align-items: center; 11 | 12 | &:not(:last-of-type) { 13 | margin-bottom: var(--grid2); 14 | } 15 | } 16 | 17 | &__message-text { 18 | margin-left: var(--grid2); 19 | } 20 | } 21 | 22 | .test-connect-result_theme_dark, 23 | .test-connect-result_theme_light { 24 | .test-connect-result { 25 | &__icon { 26 | &_type_danger { 27 | color: var(--color-red-700); 28 | } 29 | 30 | &_type_success { 31 | color: var(--color-green-700); 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/renderer/scenes/connection-modal/components/test-connect-result/test-connect-result.tsx: -------------------------------------------------------------------------------- 1 | import { IconProp } from '@fortawesome/fontawesome-svg-core'; 2 | import { faCheckCircle, faExclamationCircle } from '@fortawesome/free-solid-svg-icons'; 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 4 | import { observer } from 'mobx-react-lite'; 5 | import { ReactElement, ReactNode } from 'react'; 6 | 7 | import { useStyles } from 'renderer/lib/theme'; 8 | 9 | import { Modal } from 'renderer/ui/modal'; 10 | import { Paragraph, ParagraphSize } from 'renderer/ui/paragraph'; 11 | import { Spinner, SpinnerView } from 'renderer/ui/spinner'; 12 | 13 | import { useStore } from 'renderer/scenes/connection-modal'; 14 | 15 | import { testConnectResultTexts } from 'texts'; 16 | 17 | import styles from './test-connect-result.pcss'; 18 | 19 | export const TestConnectResult = observer((): ReactElement => { 20 | const cn = useStyles(styles, 'test-connect-result'); 21 | 22 | const store = useStore(); 23 | const { showConnectionResult, isConnecting, connectError, sshError, shouldSshConnect } = store; 24 | 25 | function handleConnectionResultClose(): void { 26 | store.setConnectionResultOpen(false); 27 | } 28 | 29 | function renderMessage(icon: IconProp, iconType: string, message: ReactNode): ReactNode { 30 | return ( 31 |
32 | 33 | 34 | {message} 35 | 36 |
37 | ); 38 | } 39 | 40 | function renderResult(prefix: string, error: Error | undefined): ReactNode { 41 | if (error) { 42 | return renderMessage(faExclamationCircle, 'danger', `${prefix}: ${error.message}`); 43 | } 44 | 45 | return renderMessage(faCheckCircle, 'success', `${prefix}: no errors`); 46 | } 47 | 48 | function renderContent(): ReactNode { 49 | if (isConnecting) { 50 | return ; 51 | } 52 | 53 | return ( 54 |
55 | {shouldSshConnect && renderResult('SSH connection', sshError)} 56 | {(!shouldSshConnect || !sshError) && renderResult('Redis connection', connectError)} 57 |
58 | ); 59 | } 60 | 61 | return ( 62 | 68 | {renderContent()} 69 | 70 | ); 71 | }); 72 | -------------------------------------------------------------------------------- /src/renderer/scenes/connection-modal/components/tls-form/index.ts: -------------------------------------------------------------------------------- 1 | export { TlsForm } from './tls-form'; 2 | -------------------------------------------------------------------------------- /src/renderer/scenes/connection-modal/components/tls-form/tls-form.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .tls-form { 4 | &__item { 5 | &:not(:last-of-type) { 6 | margin-bottom: var(--grid2); 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/renderer/scenes/connection-modal/connection-modal.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | @import url('./connection-modal_theme_dark.pcss'); 4 | @import url('./connection-modal_theme_light.pcss'); 5 | 6 | .connection-modal { 7 | max-width: 500px; 8 | 9 | &__tabs { 10 | margin-bottom: var(--grid4); 11 | } 12 | 13 | &__actions { 14 | display: flex; 15 | justify-content: flex-end; 16 | border-top: 1px solid; 17 | margin: var(--grid4) var(--grid2-neg) 0; 18 | padding: var(--grid2) var(--grid2) 0; 19 | } 20 | 21 | &__action-btn { 22 | margin-left: var(--grid3); 23 | } 24 | 25 | &__test-connection-btn { 26 | margin-right: auto; 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /src/renderer/scenes/connection-modal/connection-modal_theme_dark.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .connection-modal_theme_dark { 4 | .connection-modal { 5 | &__actions { 6 | border-top-color: var(--color-grey-900); 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/renderer/scenes/connection-modal/connection-modal_theme_light.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .connection-modal_theme_light { 4 | .connection-modal { 5 | &__actions { 6 | border-top-color: var(--color-grey-150); 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/renderer/scenes/connection-modal/index.ts: -------------------------------------------------------------------------------- 1 | import { glueStore } from 'renderer/lib/mobx'; 2 | 3 | import { RootStore } from 'renderer/stores'; 4 | 5 | import { ConnectionModalStore } from './connection-modal-store'; 6 | import { ConnectionModalView } from './connection-modal-view'; 7 | 8 | const [useStore, hoc] = glueStore( 9 | new ConnectionModalStore({ 10 | connectionsStore: RootStore.connectionsStore, 11 | connectionStore: RootStore.connectionStore, 12 | }), 13 | ); 14 | 15 | export const ConnectionModal = hoc(ConnectionModalView); 16 | export { useStore }; 17 | -------------------------------------------------------------------------------- /src/renderer/scenes/connections-list-modal/components/connections-list/components/components-list-table/components-list-table.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .components-list-table { 4 | &__address { 5 | word-break: break-word; 6 | } 7 | 8 | &__address-item { 9 | &:not(:first-of-type) { 10 | margin-top: var(--grid1); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/renderer/scenes/connections-list-modal/components/connections-list/components/components-list-table/components-list-table.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement, ReactNode } from 'react'; 2 | 3 | import { Connection } from 'data'; 4 | 5 | import { useStyles } from 'renderer/lib/theme'; 6 | 7 | import { Table, TableSize } from 'renderer/ui/table'; 8 | 9 | import styles from './components-list-table.pcss'; 10 | 11 | enum Column { 12 | Name = 'name', 13 | Address = 'address', 14 | User = 'user', 15 | } 16 | 17 | interface Props { 18 | className?: string; 19 | list: Connection[]; 20 | onConnectionClick: (connection: Connection) => void; 21 | onConnectionDoubleClick: (connection: Connection) => void; 22 | onResetConnection: () => void; 23 | active: Connection | null; 24 | } 25 | 26 | const columnName: Record = { 27 | [Column.Name]: 'Name', 28 | [Column.Address]: 'Address', 29 | [Column.User]: 'User', 30 | }; 31 | 32 | export function ComponentsListTable({ 33 | className, 34 | list, 35 | active, 36 | onConnectionClick, 37 | onConnectionDoubleClick, 38 | onResetConnection, 39 | }: Props): ReactElement { 40 | const cn = useStyles(styles, 'components-list-table'); 41 | 42 | function renderColumn(column: Column, item: Connection): ReactNode { 43 | switch (column) { 44 | case Column.Name: 45 | return item.main.name; 46 | 47 | case Column.Address: 48 | return ( 49 |
50 | {item.main.addresses.map(({ host, port }, index) => ( 51 |
{`${host}:${port}`}
52 | ))} 53 |
54 | ); 55 | 56 | case Column.User: 57 | return item.auth.username; 58 | } 59 | } 60 | 61 | return ( 62 | 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /src/renderer/scenes/connections-list-modal/components/connections-list/components/components-list-table/index.ts: -------------------------------------------------------------------------------- 1 | export { ComponentsListTable } from './components-list-table'; 2 | -------------------------------------------------------------------------------- /src/renderer/scenes/connections-list-modal/components/connections-list/connections-list.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .connections-list { 4 | min-height: 200px; 5 | 6 | &__empty { 7 | text-align: center; 8 | padding: var(--grid2) 0; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/renderer/scenes/connections-list-modal/components/connections-list/connections-list.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from 'mobx-react-lite'; 2 | import { ReactElement, ReactNode } from 'react'; 3 | 4 | import { Connection } from 'data'; 5 | 6 | import { PageState } from 'renderer/types'; 7 | 8 | import { useStyles } from 'renderer/lib/theme'; 9 | 10 | import { Paragraph, ParagraphSize } from 'renderer/ui/paragraph'; 11 | import { Spinner, SpinnerView } from 'renderer/ui/spinner'; 12 | 13 | import { useStore } from 'renderer/scenes/connections-list-modal'; 14 | 15 | import { connectionsListTexts } from 'texts'; 16 | 17 | import { ComponentsListTable } from './components/components-list-table'; 18 | 19 | import styles from './connections-list.pcss'; 20 | 21 | interface Props { 22 | onDoubleClick: () => void; 23 | className?: string; 24 | } 25 | 26 | export const ConnectionsList = observer(({ onDoubleClick, className }: Props): ReactElement => { 27 | const cn = useStyles(styles, 'connections-list'); 28 | 29 | const store = useStore(); 30 | const { sceneState, connections, selectedConnection } = store; 31 | 32 | function handleConnectionClick(connection: Connection): void { 33 | store.setSelected(connection); 34 | } 35 | 36 | function handleConnectionConnect(connection: Connection): void { 37 | store.setSelected(connection); 38 | onDoubleClick(); 39 | } 40 | 41 | function handleResetConnection(): void { 42 | store.setSelected(null); 43 | } 44 | 45 | if (sceneState === PageState.LOADING) { 46 | return ; 47 | } 48 | 49 | function renderNoConnectionsText(): ReactNode { 50 | if (connections.length) { 51 | return null; 52 | } 53 | 54 | return ( 55 | 56 | {connectionsListTexts.noConnections} 57 | 58 | ); 59 | } 60 | 61 | return ( 62 |
63 | 71 | 72 | {renderNoConnectionsText()} 73 |
74 | ); 75 | }); 76 | -------------------------------------------------------------------------------- /src/renderer/scenes/connections-list-modal/components/connections-list/index.ts: -------------------------------------------------------------------------------- 1 | export { ConnectionsList } from './connections-list'; 2 | -------------------------------------------------------------------------------- /src/renderer/scenes/connections-list-modal/connections-list-modal.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .connections-list-modal { 4 | max-width: 600px; 5 | 6 | &__delete-btn { 7 | margin-right: auto; 8 | } 9 | 10 | &__action-btn { 11 | margin-left: var(--grid2); 12 | } 13 | 14 | &__connections { 15 | margin-top: var(--grid1); 16 | } 17 | 18 | &__actions { 19 | display: flex; 20 | margin-top: var(--grid4); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/scenes/connections-list-modal/index.ts: -------------------------------------------------------------------------------- 1 | import { glueStore } from 'renderer/lib/mobx'; 2 | 3 | import { RootStore } from 'renderer/stores'; 4 | 5 | import { ConnectionsListModalStore } from './connections-list-modal-store'; 6 | import { ConnectionsListModalView } from './connections-list-modal-view'; 7 | 8 | const [useStore, hoc] = glueStore( 9 | new ConnectionsListModalStore({ 10 | connectionsStore: RootStore.connectionsStore, 11 | }), 12 | ); 13 | 14 | export const ConnectionsListModal = hoc(ConnectionsListModalView); 15 | export { useStore }; 16 | -------------------------------------------------------------------------------- /src/renderer/scenes/edit-value-form/edit-value-form.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .edit-value-form { 4 | &__key-wrap { 5 | display: flex; 6 | align-items: flex-end; 7 | } 8 | 9 | &__key { 10 | flex: 1; 11 | } 12 | 13 | &__edit-btn { 14 | margin-left: var(--grid2); 15 | margin-bottom: var(--grid2); 16 | } 17 | 18 | &__create-wrap { 19 | display: flex; 20 | justify-content: space-between; 21 | align-items: center; 22 | margin-bottom: var(--grid3); 23 | } 24 | 25 | &__form { 26 | display: flex; 27 | flex-direction: column; 28 | height: 100%; 29 | } 30 | 31 | &__ttl { 32 | margin-top: var(--grid2); 33 | } 34 | 35 | &__textarea { 36 | flex: 1; 37 | margin-top: var(--grid2); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/renderer/scenes/edit-value-form/index.ts: -------------------------------------------------------------------------------- 1 | import { glueStore } from 'renderer/lib/mobx'; 2 | 3 | import { RootStore } from 'renderer/stores/root'; 4 | 5 | import { EditValueFormStore } from './edit-value-form-store'; 6 | import { EditValueFormView } from './edit-value-form-view'; 7 | 8 | const [useStore, hoc] = glueStore( 9 | new EditValueFormStore({ 10 | valueTabsStore: RootStore.valueTabsStore, 11 | connectionsDataStore: RootStore.connectionsDataStore, 12 | }), 13 | ); 14 | 15 | export const EditValueForm = hoc(EditValueFormView); 16 | export { useStore }; 17 | -------------------------------------------------------------------------------- /src/renderer/scenes/edit-value-form/types.ts: -------------------------------------------------------------------------------- 1 | import { Redis } from 'renderer/lib/redis'; 2 | 3 | export enum EditDataField { 4 | RedisId = 'redisId', 5 | Key = 'key', 6 | CanEditKey = 'canEditKey', 7 | Ttl = 'ttl', 8 | Value = 'value', 9 | } 10 | 11 | export interface EditDataValues { 12 | [EditDataField.RedisId]: string; 13 | [EditDataField.Key]: string; 14 | [EditDataField.CanEditKey]: boolean; 15 | [EditDataField.Ttl]?: number; 16 | [EditDataField.Value]: string; 17 | } 18 | 19 | export interface Props { 20 | connections: Redis[]; 21 | } 22 | -------------------------------------------------------------------------------- /src/renderer/scenes/edit-value-form/validation.ts: -------------------------------------------------------------------------------- 1 | import { yup } from 'renderer/lib/yup'; 2 | 3 | export const validationSchema = yup.object().shape({ 4 | key: yup.string().required(), 5 | redisId: yup.string().required(), 6 | ttl: yup.number(), 7 | value: yup.string().required(), 8 | }); 9 | -------------------------------------------------------------------------------- /src/renderer/scenes/main-page/components/open-connections-list/index.ts: -------------------------------------------------------------------------------- 1 | export { OpenConnectionsList } from './open-connections-list'; 2 | -------------------------------------------------------------------------------- /src/renderer/scenes/main-page/components/open-connections-list/open-connections-list.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .open-connections-list { 4 | padding: var(--grid2); 5 | } 6 | -------------------------------------------------------------------------------- /src/renderer/scenes/main-page/components/open-connections-list/open-connections-list.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from 'mobx-react-lite'; 2 | import { ReactElement } from 'react'; 3 | 4 | import { useStyles } from 'renderer/lib/theme'; 5 | 6 | import { useStore } from 'renderer/scenes/main-page'; 7 | import { OpenConnection } from 'renderer/scenes/open-connection'; 8 | 9 | import styles from './open-connections-list.pcss'; 10 | 11 | export const OpenConnectionsList = observer((): ReactElement => { 12 | const cn = useStyles(styles, 'open-connections-list'); 13 | 14 | const store = useStore(); 15 | const { openConnections } = store; 16 | 17 | return ( 18 |
19 | {openConnections.map((connection, index) => ( 20 | 21 | ))} 22 |
23 | ); 24 | }); 25 | -------------------------------------------------------------------------------- /src/renderer/scenes/main-page/components/resizable-layout/index.ts: -------------------------------------------------------------------------------- 1 | export { ResizableLayout } from './resizable-layout'; 2 | -------------------------------------------------------------------------------- /src/renderer/scenes/main-page/components/resizable-layout/resizable-layout.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | @import url('./resizable-layout_theme_dark.pcss'); 4 | @import url('./resizable-layout_theme_light.pcss'); 5 | 6 | .resizable-layout { 7 | display: flex; 8 | flex: 1; 9 | overflow: hidden; 10 | 11 | &_active { 12 | cursor: ew-resize; 13 | } 14 | 15 | &__left { 16 | min-width: 200px; 17 | max-height: 100%; 18 | overflow: auto; 19 | position: relative; 20 | border-right: 1px solid; 21 | } 22 | 23 | &__right { 24 | min-width: 200px; 25 | flex: 1; 26 | padding: var(--grid2); 27 | } 28 | 29 | &__left, 30 | &__right { 31 | &_active { 32 | pointer-events: none; 33 | } 34 | } 35 | 36 | &__resizer { 37 | position: absolute; 38 | z-index: 2; 39 | top: 0; 40 | bottom: 0; 41 | right: var(--grid0_5-neg); 42 | width: var(--grid1); 43 | cursor: ew-resize; 44 | opacity: 0; 45 | transition: 0.5s opacity; 46 | outline: 0; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/renderer/scenes/main-page/components/resizable-layout/resizable-layout_theme_dark.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .resizable-layout_theme_dark { 4 | .resizable-layout { 5 | &__left { 6 | border-right-color: var(--color-grey-900); 7 | } 8 | 9 | &__resizer { 10 | background: var(--color-blue-700); 11 | 12 | &:hover { 13 | opacity: 1; 14 | } 15 | 16 | &_active { 17 | opacity: 1; 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/renderer/scenes/main-page/components/resizable-layout/resizable-layout_theme_light.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .resizable-layout_theme_light { 4 | color: var(--color-grey-800); 5 | background-color: var(--color-white); 6 | 7 | .resizable-layout { 8 | &__left { 9 | border-right-color: var(--color-grey-150); 10 | } 11 | 12 | &__resizer { 13 | background: var(--color-blue-500); 14 | 15 | &:hover { 16 | opacity: 1; 17 | } 18 | 19 | &_active { 20 | opacity: 1; 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/renderer/scenes/main-page/components/top-actions/index.ts: -------------------------------------------------------------------------------- 1 | export { TopActions } from './top-actions'; 2 | -------------------------------------------------------------------------------- /src/renderer/scenes/main-page/components/top-actions/top-actions.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | @import url('./top-actions_theme_dark.pcss'); 4 | @import url('./top-actions_theme_light.pcss'); 5 | 6 | .top-actions { 7 | padding: var(--grid2) var(--grid3); 8 | border-bottom-width: 1px; 9 | border-bottom-style: solid; 10 | display: flex; 11 | 12 | &__actions-group { 13 | &:not(:first-of-type) { 14 | border-left: 2px solid; 15 | padding-left: var(--grid3); 16 | margin-left: var(--grid3); 17 | } 18 | } 19 | 20 | &__action { 21 | &:not(:first-of-type) { 22 | margin-left: var(--grid3); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/renderer/scenes/main-page/components/top-actions/top-actions.tsx: -------------------------------------------------------------------------------- 1 | import { IconProp } from '@fortawesome/fontawesome-svg-core'; 2 | import { faBan, faEject, faServer, faTrash } from '@fortawesome/free-solid-svg-icons'; 3 | import { ReactElement, ReactNode } from 'react'; 4 | 5 | import { useStyles } from 'renderer/lib/theme'; 6 | 7 | import { ButtonIcon } from 'renderer/ui/button-icon'; 8 | 9 | import styles from './top-actions.pcss'; 10 | 11 | interface Props { 12 | hasSelectedItem: boolean; 13 | hasActiveTab: boolean; 14 | isDisconnecting: boolean; 15 | isDeleting: boolean; 16 | onOpenConnections: () => void; 17 | onDisconnect: () => void; 18 | onCancelSelect: () => void; 19 | onDeleteKey: () => void; 20 | } 21 | 22 | interface Action { 23 | icon: IconProp; 24 | onClick: () => void; 25 | disabled?: boolean; 26 | } 27 | 28 | export function TopActions({ 29 | hasSelectedItem, 30 | hasActiveTab, 31 | isDisconnecting, 32 | isDeleting, 33 | onOpenConnections, 34 | onDisconnect, 35 | onCancelSelect, 36 | onDeleteKey, 37 | }: Props): ReactElement { 38 | const cn = useStyles(styles, 'top-actions'); 39 | 40 | function renderKeyActions(actions: Action[]): ReactNode { 41 | return ( 42 |
43 | {actions.map(({ icon, onClick }, index) => ( 44 | 45 | ))} 46 |
47 | ); 48 | } 49 | 50 | return ( 51 |
52 | {renderKeyActions([ 53 | { 54 | icon: faServer, 55 | onClick: onOpenConnections, 56 | }, 57 | ])} 58 | {hasSelectedItem && 59 | renderKeyActions([ 60 | { 61 | icon: faEject, 62 | onClick: onDisconnect, 63 | disabled: isDisconnecting, 64 | }, 65 | ])} 66 | {hasActiveTab && 67 | renderKeyActions([ 68 | { 69 | icon: faBan, 70 | onClick: onCancelSelect, 71 | disabled: isDeleting || isDisconnecting, 72 | }, 73 | { 74 | icon: faTrash, 75 | onClick: onDeleteKey, 76 | disabled: isDeleting || isDisconnecting, 77 | }, 78 | ])} 79 |
80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /src/renderer/scenes/main-page/components/top-actions/top-actions_theme_dark.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .top-actions_theme_dark { 4 | background: var(--color-grey-850); 5 | border-bottom-color: var(--color-grey-900); 6 | 7 | .top-actions { 8 | &__actions-group { 9 | &:not(:first-of-type) { 10 | border-left-color: var(--color-grey-700); 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/renderer/scenes/main-page/components/top-actions/top-actions_theme_light.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .top-actions_theme_light { 4 | background: var(--color-grey-200); 5 | border-bottom-color: var(--color-grey-400); 6 | 7 | .top-actions { 8 | &__actions-group { 9 | &:not(:first-of-type) { 10 | border-left-color: var(--color-grey-700); 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/renderer/scenes/main-page/index.ts: -------------------------------------------------------------------------------- 1 | import { glueStore } from 'renderer/lib/mobx'; 2 | 3 | import { RootStore } from 'renderer/stores'; 4 | 5 | import { MainPageStore } from './main-page-store'; 6 | import { MainPageView } from './main-page-view'; 7 | 8 | const [useStore, hoc] = glueStore( 9 | new MainPageStore({ 10 | valueTabsStore: RootStore.valueTabsStore, 11 | connectionsDataStore: RootStore.connectionsDataStore, 12 | }), 13 | ); 14 | 15 | export const MainPage = hoc(MainPageView); 16 | export { useStore }; 17 | -------------------------------------------------------------------------------- /src/renderer/scenes/main-page/main-page-view.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from 'mobx-react-lite'; 2 | import { ReactElement, useEffect } from 'react'; 3 | 4 | import { Redis } from 'renderer/lib/redis'; 5 | import { useStyles } from 'renderer/lib/theme'; 6 | 7 | import { ConnectionsListModal } from 'renderer/scenes/connections-list-modal'; 8 | import { EditValueForm } from 'renderer/scenes/edit-value-form'; 9 | 10 | import { OpenConnectionsList } from './components/open-connections-list'; 11 | import { ResizableLayout } from './components/resizable-layout'; 12 | import { TopActions } from './components/top-actions'; 13 | 14 | import { useStore } from './index'; 15 | 16 | import styles from './main-page.pcss'; 17 | 18 | export const MainPageView = observer((): ReactElement => { 19 | const cn = useStyles(styles, 'main-page'); 20 | 21 | const pageStore = useStore(); 22 | const { connectionsListOpened, openConnections, hasActiveTab, hasSelectedItem, isDeleting, isDisconnecting } = 23 | pageStore; 24 | 25 | useEffect(() => { 26 | const unsubscribe = window.electron.menu.on('OPEN_CONNECTIONS_LIST', handleOpenConnections); 27 | 28 | return () => { 29 | unsubscribe(); 30 | }; 31 | }, []); 32 | 33 | function handleOpenConnections(): void { 34 | pageStore.setConnectionsListOpened(true); 35 | } 36 | 37 | function handleConnectionsModalClose(): void { 38 | pageStore.setConnectionsListOpened(false); 39 | } 40 | 41 | function handleConnect(redis: Redis): void { 42 | pageStore.addOpenConnection(redis); 43 | } 44 | 45 | function handleDeleteKey(): void { 46 | pageStore.deleteActiveKey(); 47 | } 48 | 49 | function handleCancelSelect(): void { 50 | pageStore.cancelActiveKey(); 51 | } 52 | 53 | function handleDisconnectClick(): void { 54 | pageStore.disconnectConnection(); 55 | } 56 | 57 | return ( 58 |
59 | 69 | 70 | } 72 | right={openConnections.length > 0 && } 73 | /> 74 | 75 | 80 |
81 | ); 82 | }); 83 | -------------------------------------------------------------------------------- /src/renderer/scenes/main-page/main-page.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | @import url('./main-page_theme_dark.pcss'); 4 | @import url('./main-page_theme_light.pcss'); 5 | 6 | .main-page { 7 | height: 100vh; 8 | display: flex; 9 | flex-direction: column; 10 | } 11 | 12 | -------------------------------------------------------------------------------- /src/renderer/scenes/main-page/main-page_theme_dark.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .main-page_theme_dark { 4 | color: var(--color-grey-400); 5 | background-color: var(--color-grey-800); 6 | } 7 | -------------------------------------------------------------------------------- /src/renderer/scenes/main-page/main-page_theme_light.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .main-page_theme_light { 4 | color: var(--color-grey-800); 5 | background-color: var(--color-white); 6 | } 7 | -------------------------------------------------------------------------------- /src/renderer/scenes/open-connection/index.ts: -------------------------------------------------------------------------------- 1 | import { glueStore } from 'renderer/lib/mobx'; 2 | 3 | import { RootStore } from 'renderer/stores/root'; 4 | 5 | import { OpenConnectionsStore } from './open-connection-store'; 6 | import { OpenConnectionView } from './open-connection-view'; 7 | 8 | const [useStore, hoc] = glueStore( 9 | new OpenConnectionsStore({ 10 | connectionsDataStore: RootStore.connectionsDataStore, 11 | valueTabsStore: RootStore.valueTabsStore, 12 | }), 13 | ); 14 | 15 | export const OpenConnection = hoc(OpenConnectionView); 16 | export { useStore }; 17 | -------------------------------------------------------------------------------- /src/renderer/scenes/open-connection/open-connection.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | @import url('./open-connection_theme_dark.pcss'); 4 | @import url('./open-connection_theme_light.pcss'); 5 | 6 | .open-connection { 7 | &:not(:last-of-type) { 8 | margin-bottom: var(--grid2); 9 | } 10 | 11 | &__connection, 12 | &__key { 13 | display: flex; 14 | align-items: center; 15 | position: relative; 16 | z-index: 2; 17 | outline: none; 18 | cursor: pointer; 19 | 20 | & > * { 21 | z-index: 2; 22 | } 23 | 24 | &_selected { 25 | &::before { 26 | content: ''; 27 | position: absolute; 28 | top: var(--grid0_5-neg); 29 | bottom: var(--grid0_5-neg); 30 | left: -1000px; 31 | right: -1000px; 32 | z-index: 1; 33 | } 34 | } 35 | } 36 | 37 | &__arrow-icon { 38 | margin-right: var(--grid2); 39 | transition: all 0.2s; 40 | 41 | &_open { 42 | transform: rotate(90deg); 43 | } 44 | } 45 | 46 | &__name { 47 | white-space: nowrap; 48 | text-overflow: ellipsis; 49 | overflow: hidden; 50 | margin-left: var(--grid1); 51 | font-size: var(--font-size-300); 52 | line-height: var(--font-size-500); 53 | 54 | &_active { 55 | font-style: italic; 56 | font-weight: var(--font-weight-600); 57 | } 58 | } 59 | 60 | &__content { 61 | margin-left: var(--grid1); 62 | padding: var(--grid1) 0 var(--grid0_5) var(--grid3); 63 | border-left: 1px dashed; 64 | } 65 | 66 | &__data-item { 67 | &:not(:last-of-type) { 68 | margin-bottom: var(--grid1_5); 69 | } 70 | 71 | &:first-of-type { 72 | margin-top: var(--grid0_5); 73 | } 74 | } 75 | 76 | &__key { 77 | cursor: pointer; 78 | user-select: none; 79 | } 80 | 81 | &__delete-btn { 82 | margin-left: auto; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/renderer/scenes/open-connection/open-connection_theme_dark.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .open-connection_theme_dark { 4 | .open-connection { 5 | &__content { 6 | border-left-color: var(--color-grey-400); 7 | } 8 | 9 | &__key { 10 | &_active { 11 | color: var(--color-grey-600); 12 | } 13 | } 14 | 15 | &__key, 16 | &__connection { 17 | &_selected { 18 | &::before { 19 | background: var(--color-grey-900); 20 | opacity: var(--opacity-200); 21 | } 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/renderer/scenes/open-connection/open-connection_theme_light.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .open-connection_theme_light { 4 | .open-connection { 5 | &__content { 6 | border-left-color: var(--color-grey-400); 7 | } 8 | 9 | &__key { 10 | &_active { 11 | color: var(--color-grey-600); 12 | } 13 | } 14 | 15 | &__key, 16 | &__connection { 17 | &_selected { 18 | &::before { 19 | background: var(--color-grey-400); 20 | opacity: var(--opacity-200); 21 | } 22 | } 23 | } 24 | 25 | &__icon { 26 | color: var(--color-grey-700); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/renderer/scenes/open-connection/types.ts: -------------------------------------------------------------------------------- 1 | import { Redis } from 'renderer/lib/redis'; 2 | 3 | export interface Props { 4 | redis: Redis; 5 | } 6 | 7 | export enum IconType { 8 | Database = 'database', 9 | Prefix = 'prefix', 10 | Key = 'key', 11 | } 12 | -------------------------------------------------------------------------------- /src/renderer/stores/config.ts: -------------------------------------------------------------------------------- 1 | import { configure } from 'mobx'; 2 | 3 | configure({ 4 | enforceActions: 'always', 5 | computedRequiresReaction: true, 6 | reactionRequiresObservable: true, 7 | observableRequiresReaction: true, 8 | disableErrorBoundaries: true, 9 | }); 10 | -------------------------------------------------------------------------------- /src/renderer/stores/connection-store.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from 'data'; 2 | 3 | import { connectionsClient } from 'renderer/data'; 4 | 5 | export class ConnectionStore { 6 | async createOrUpdate(data: Omit, id?: string): Promise { 7 | let connectionData: Connection; 8 | 9 | if (id) { 10 | connectionData = await connectionsClient.update(id, data); 11 | } else { 12 | connectionData = await connectionsClient.create(data); 13 | } 14 | 15 | return connectionData; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/renderer/stores/connections-store.ts: -------------------------------------------------------------------------------- 1 | import { action, computed, makeObservable, observable, runInAction } from 'mobx'; 2 | 3 | import { Connection } from 'data'; 4 | 5 | import { connectionsClient } from 'renderer/data'; 6 | 7 | export class ConnectionsStore { 8 | @observable 9 | private _connections?: Connection[]; 10 | 11 | constructor() { 12 | makeObservable(this); 13 | } 14 | 15 | @computed 16 | get connections(): Connection[] | undefined { 17 | return this._connections; 18 | } 19 | 20 | async loadData(): Promise { 21 | const list = await connectionsClient.list(); 22 | 23 | runInAction(() => { 24 | this._connections = list; 25 | }); 26 | } 27 | 28 | async deleteConnection(id: string): Promise { 29 | if (!this._connections?.some((conn) => conn.id === id)) { 30 | return; 31 | } 32 | 33 | await connectionsClient.deleteItem(id); 34 | 35 | runInAction(() => { 36 | this._connections = this._connections?.filter((conn) => conn.id !== id); 37 | }); 38 | } 39 | 40 | @action 41 | setConnection(connection: Connection): void { 42 | const hasConnection = this._connections?.some(({ id }) => id === connection.id); 43 | if (hasConnection) { 44 | this._connections = this._connections?.map((conn) => (conn.id === connection.id ? connection : conn)); 45 | } else { 46 | this._connections?.push(connection); 47 | } 48 | } 49 | 50 | async cloneConnection(id: string): Promise { 51 | const connection = this._connections?.find((conn) => conn.id === id); 52 | 53 | if (!connection) { 54 | return; 55 | } 56 | 57 | // eslint-disable-next-line 58 | const { id: connectionId, ...clonedConnection } = connection; 59 | const connectionData = await connectionsClient.create(JSON.parse(JSON.stringify(clonedConnection))); 60 | this._connections?.push(connectionData); 61 | } 62 | 63 | @action 64 | dispose(): void { 65 | this._connections = undefined; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/renderer/stores/index.ts: -------------------------------------------------------------------------------- 1 | export { ConnectionsStore } from './connections-store'; 2 | export { ConnectionStore } from './connection-store'; 3 | export { ConnectionsDataStore, ConnectionDataStore } from './connections-data-store'; 4 | export { ValueTabsStore } from './value-tabs-store'; 5 | export type { ConnectionData } from './connections-data-store'; 6 | 7 | export { RootStore } from './root'; 8 | -------------------------------------------------------------------------------- /src/renderer/stores/root.ts: -------------------------------------------------------------------------------- 1 | import { ConnectionStore } from './connection-store'; 2 | import { ConnectionsDataStore } from './connections-data-store'; 3 | import { ConnectionsStore } from './connections-store'; 4 | import { ValueTabsStore } from './value-tabs-store'; 5 | 6 | const valueTabsStore = new ValueTabsStore(); 7 | 8 | export const RootStore = { 9 | connectionsStore: new ConnectionsStore(), 10 | connectionStore: new ConnectionStore(), 11 | valueTabsStore, 12 | connectionsDataStore: new ConnectionsDataStore({ valueTabsStore }), 13 | }; 14 | -------------------------------------------------------------------------------- /src/renderer/stores/value-tabs-store.ts: -------------------------------------------------------------------------------- 1 | import { action, computed, makeObservable, observable } from 'mobx'; 2 | 3 | import { listToKey } from 'lib/key'; 4 | 5 | import { KeyData } from 'main/lib/redis'; 6 | 7 | interface TabData { 8 | redisId: string; 9 | prefix: string[]; 10 | } 11 | 12 | export class ValueTabsStore { 13 | @observable 14 | private _activeTab?: TabData; 15 | 16 | @observable 17 | private _selectedPrefix?: TabData; 18 | 19 | @observable 20 | private _data?: KeyData; 21 | 22 | constructor() { 23 | makeObservable(this); 24 | } 25 | 26 | @computed 27 | get activeTab(): TabData | undefined { 28 | return this._activeTab; 29 | } 30 | 31 | @computed 32 | get selectedPrefix(): TabData | undefined { 33 | return this._selectedPrefix; 34 | } 35 | 36 | @computed 37 | get activeKeyData(): KeyData | undefined { 38 | return this._data; 39 | } 40 | 41 | @action 42 | setActiveTab(redisId: string, prefix: string[]): void { 43 | this._activeTab = { 44 | redisId, 45 | prefix, 46 | }; 47 | } 48 | 49 | @action 50 | setSelectedPrefix(redisId: string, prefix: string[]): void { 51 | this._selectedPrefix = { 52 | redisId, 53 | prefix, 54 | }; 55 | } 56 | 57 | @action 58 | setActiveData(data?: KeyData): void { 59 | this._data = data; 60 | } 61 | 62 | @action 63 | removeActiveTab(unselectIfSelected?: boolean): void { 64 | if ( 65 | unselectIfSelected && 66 | this._activeTab?.redisId === this._selectedPrefix?.redisId && 67 | listToKey(this._activeTab?.prefix) === listToKey(this._selectedPrefix?.prefix) 68 | ) { 69 | this._selectedPrefix = undefined; 70 | } 71 | 72 | this._activeTab = undefined; 73 | } 74 | 75 | @action 76 | removeSelectedPrefix(): void { 77 | if ( 78 | this._activeTab?.redisId === this._selectedPrefix?.redisId && 79 | listToKey(this._activeTab?.prefix) === listToKey(this._selectedPrefix?.prefix) 80 | ) { 81 | this._activeTab = undefined; 82 | } 83 | 84 | this._selectedPrefix = undefined; 85 | } 86 | 87 | @action 88 | dispose(): void { 89 | this._activeTab = undefined; 90 | this._data = undefined; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/renderer/styles/reset.pcss: -------------------------------------------------------------------------------- 1 | @import url('normalize.css'); 2 | @import url('./font-family.pcss'); 3 | @import url('./variables.pcss'); 4 | 5 | html, 6 | body { 7 | font-family: var(--font); 8 | font-size: var(--font-size-400); 9 | -webkit-tap-highlight-color: rgb(0 0 0 / 0%); 10 | text-size-adjust: none; 11 | overflow-x: hidden; 12 | min-height: 100vh; 13 | } 14 | -------------------------------------------------------------------------------- /src/renderer/styles/variables.pcss: -------------------------------------------------------------------------------- 1 | @import url('./vars/border.pcss'); 2 | @import url('./vars/color.pcss'); 3 | @import url('./vars/font.pcss'); 4 | @import url('./vars/grid.pcss'); 5 | @import url('./vars/opacity.pcss'); 6 | 7 | :root { 8 | /* stylelint-disable-next-line at-rule-no-unknown */ 9 | @import-json '../mq.json'; 10 | } 11 | -------------------------------------------------------------------------------- /src/renderer/styles/vars/border.pcss: -------------------------------------------------------------------------------- 1 | :root { 2 | --border-radius-100: 2px; 3 | --border-radius-200: 4px; 4 | --border-radius-300: 8px; 5 | --border-radius-400: 12px; 6 | } 7 | -------------------------------------------------------------------------------- /src/renderer/styles/vars/font.pcss: -------------------------------------------------------------------------------- 1 | :root { 2 | --font: 'Open Sans', 'Helvetica Neue', arial, helvetica, sans-serif; 3 | 4 | /** 5 | * Font sizes 6 | */ 7 | --font-size-50: 8px; 8 | --font-size-100: 10px; 9 | --font-size-200: 12px; 10 | --font-size-300: 13px; 11 | --font-size-400: 14px; 12 | --font-size-500: 16px; 13 | --font-size-600: 18px; 14 | --font-size-700: 20px; 15 | --font-size-800: 24px; 16 | --font-size-900: 28px; 17 | --font-size-1000: 32px; 18 | 19 | /** 20 | * Font weights 21 | */ 22 | --font-weight-300: 300; 23 | --font-weight-400: 400; 24 | --font-weight-600: 600; 25 | 26 | /** 27 | * Line heights 28 | */ 29 | --line-height-100: 1; 30 | --line-height-200: 1.2; 31 | --line-height-300: 1.4; 32 | --line-height-400: 1.6; 33 | } 34 | -------------------------------------------------------------------------------- /src/renderer/styles/vars/grid.pcss: -------------------------------------------------------------------------------- 1 | :root { 2 | --grid5-neg: -20px; 3 | --grid4-neg: -16px; 4 | --grid3-neg: -12px; 5 | --grid2-neg: -8px; 6 | --grid1-neg: -4px; 7 | --grid0_5-neg: -2px; 8 | --grid0_5: 2px; 9 | --grid1: 4px; 10 | --grid1_5: 6px; 11 | --grid2: 8px; 12 | --grid2_5: 10px; 13 | --grid3: 12px; 14 | --grid4: 16px; 15 | --grid5: 20px; 16 | --grid6: 24px; 17 | --grid7: 28px; 18 | --grid8: 32px; 19 | --grid9: 36px; 20 | --grid10: 40px; 21 | --grid11: 44px; 22 | --grid12: 48px; 23 | --grid13: 52px; 24 | --grid14: 56px; 25 | --grid15: 60px; 26 | --grid16: 64px; 27 | --grid17: 68px; 28 | --grid18: 72px; 29 | --grid19: 76px; 30 | --grid20: 80px; 31 | --grid21: 84px; 32 | --grid22: 88px; 33 | } 34 | -------------------------------------------------------------------------------- /src/renderer/styles/vars/opacity.pcss: -------------------------------------------------------------------------------- 1 | :root { 2 | --opacity-50: 1; 3 | --opacity-100: 0.9; 4 | --opacity-150: 0.8; 5 | --opacity-200: 0.6; 6 | --opacity-300: 0.4; 7 | --opacity-400: 0.25; 8 | } 9 | -------------------------------------------------------------------------------- /src/renderer/types/error.ts: -------------------------------------------------------------------------------- 1 | export type AnyError = Error; 2 | -------------------------------------------------------------------------------- /src/renderer/types/index.ts: -------------------------------------------------------------------------------- 1 | export { PageState } from './page'; 2 | export type { AnyError } from './error'; 3 | -------------------------------------------------------------------------------- /src/renderer/types/page.ts: -------------------------------------------------------------------------------- 1 | export enum PageState { 2 | LOADING = 'LOADING', 3 | READY = 'READY', 4 | ERROR = 'ERROR', 5 | } 6 | -------------------------------------------------------------------------------- /src/renderer/ui/button-icon/button-icon.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | @import url("./button-icon_theme_dark.pcss"); 4 | @import url("./button-icon_theme_light.pcss"); 5 | 6 | .button-icon { 7 | cursor: pointer; 8 | 9 | &_disabled { 10 | pointer-events: none; 11 | opacity: var(--opacity-200); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/renderer/ui/button-icon/button-icon.tsx: -------------------------------------------------------------------------------- 1 | import { IconProp, SizeProp } from '@fortawesome/fontawesome-svg-core'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import classnames from 'classnames'; 4 | import { ReactElement } from 'react'; 5 | 6 | import { useStyles } from 'renderer/lib/theme'; 7 | 8 | import styles from './button-icon.pcss'; 9 | 10 | export enum ButtonIconView { 11 | Default = 'default', 12 | Success = 'success', 13 | Danger = 'danger', 14 | } 15 | 16 | interface Props { 17 | className?: string; 18 | size?: SizeProp; 19 | icon: IconProp; 20 | view: ButtonIconView; 21 | onClick?: () => void; 22 | disabled?: boolean; 23 | } 24 | 25 | export function ButtonIcon({ className, size, view, icon, disabled, onClick }: Props): ReactElement { 26 | const cn = useStyles(styles, 'button-icon'); 27 | 28 | function handleClick(): void { 29 | if (disabled) { 30 | return; 31 | } 32 | 33 | onClick?.(); 34 | } 35 | 36 | return ( 37 | 43 | ); 44 | } 45 | 46 | ButtonIcon.defaultProps = { 47 | view: ButtonIconView.Default, 48 | }; 49 | -------------------------------------------------------------------------------- /src/renderer/ui/button-icon/button-icon_theme_dark.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .button-icon_theme_dark { 4 | &.button-icon { 5 | opacity: var(--opacity-200); 6 | 7 | &:hover { 8 | opacity: var(--opacity-50); 9 | } 10 | 11 | &_view_default { 12 | color: var(--color-white); 13 | } 14 | 15 | &_view_danger { 16 | color: var(--color-red-700); 17 | opacity: var(--opacity-100); 18 | 19 | &:hover { 20 | opacity: var(--opacity-50); 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/renderer/ui/button-icon/button-icon_theme_light.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .button-icon_theme_light { 4 | &.button-icon { 5 | opacity: var(--opacity-50); 6 | 7 | &:hover { 8 | opacity: var(--opacity-150); 9 | } 10 | 11 | &_view_default { 12 | color: var(--color-grey-600); 13 | } 14 | 15 | &_view_danger { 16 | color: var(--color-red-700); 17 | opacity: var(--opacity-100); 18 | 19 | &:hover { 20 | opacity: var(--opacity-50); 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/renderer/ui/button-icon/index.ts: -------------------------------------------------------------------------------- 1 | export type { SizeProp } from '@fortawesome/fontawesome-svg-core'; 2 | export { ButtonIcon, ButtonIconView } from './button-icon'; 3 | -------------------------------------------------------------------------------- /src/renderer/ui/button/button.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | @import url('./button_theme_dark.pcss'); 4 | @import url('./button_theme_light.pcss'); 5 | 6 | .button { 7 | border-radius: var(--border-radius-200); 8 | border: 1px solid; 9 | cursor: pointer; 10 | line-height: var(--grid6); 11 | outline: 0; 12 | padding: 0; 13 | overflow: hidden; 14 | 15 | &_size_s { 16 | font-size: var(--font-size-200); 17 | } 18 | 19 | &_size_m { 20 | font-size: var(--font-size-400); 21 | } 22 | 23 | &_disabled { 24 | pointer-events: none; 25 | opacity: var(--opacity-200); 26 | } 27 | 28 | &__icon { 29 | margin-right: var(--grid2); 30 | } 31 | 32 | &__content { 33 | position: relative; 34 | display: flex; 35 | align-items: center; 36 | padding: 0 var(--grid2); 37 | 38 | &_loading { 39 | pointer-events: none; 40 | 41 | &::before { 42 | content: ''; 43 | position: absolute; 44 | top: 0; 45 | right: 0; 46 | bottom: 0; 47 | left: 0; 48 | } 49 | } 50 | } 51 | 52 | &__spinner { 53 | position: absolute; 54 | z-index: 2; 55 | top: 50%; 56 | left: 50%; 57 | transform: translate(-50%, -50%); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/renderer/ui/button/button.tsx: -------------------------------------------------------------------------------- 1 | import { IconProp, SizeProp } from '@fortawesome/fontawesome-svg-core'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import classnames from 'classnames'; 4 | import { ReactElement, ReactNode } from 'react'; 5 | 6 | import { useStyles } from 'renderer/lib/theme'; 7 | 8 | import { Spinner, SpinnerSize } from 'renderer/ui/spinner'; 9 | 10 | import styles from './button.pcss'; 11 | 12 | export enum ButtonSize { 13 | S = 's', 14 | M = 'm', 15 | } 16 | 17 | export enum ButtonView { 18 | Default = 'default', 19 | Success = 'success', 20 | Danger = 'danger', 21 | } 22 | 23 | type ButtonType = 'submit' | 'button' | 'reset'; 24 | 25 | interface Props { 26 | children?: ReactNode; 27 | className?: string; 28 | size: ButtonSize; 29 | view: ButtonView; 30 | onClick?: () => void; 31 | icon?: IconProp; 32 | disabled?: boolean; 33 | isLoading?: boolean; 34 | type?: ButtonType; 35 | } 36 | 37 | const mapSizeToIconSize: Record = { 38 | [ButtonSize.S]: 'sm', 39 | [ButtonSize.M]: '1x', 40 | }; 41 | 42 | const mapSizeToSpinnerSize: Record = { 43 | [ButtonSize.S]: SpinnerSize.XS, 44 | [ButtonSize.M]: SpinnerSize.S, 45 | }; 46 | 47 | export function Button({ 48 | children, 49 | className, 50 | size, 51 | view, 52 | onClick, 53 | icon, 54 | disabled, 55 | isLoading, 56 | type, 57 | }: Props): ReactElement { 58 | const cn = useStyles(styles, 'button'); 59 | 60 | function handleClick(): void { 61 | if (disabled) { 62 | return; 63 | } 64 | 65 | onClick?.(); 66 | } 67 | 68 | function renderIcon(): ReactNode { 69 | if (!icon) { 70 | return null; 71 | } 72 | 73 | return ( 74 | 75 | 76 | 77 | ); 78 | } 79 | 80 | function renderLoading(): ReactNode { 81 | if (!isLoading) { 82 | return null; 83 | } 84 | 85 | return ; 86 | } 87 | 88 | return ( 89 | 96 | ); 97 | } 98 | 99 | Button.defaultProps = { 100 | size: ButtonSize.M, 101 | view: ButtonView.Default, 102 | }; 103 | -------------------------------------------------------------------------------- /src/renderer/ui/button/button_theme_dark.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .button_theme_dark { 4 | &.button { 5 | &_view_default { 6 | background: var(--color-grey-750); 7 | border-color: var(--color-grey-900); 8 | color: var(--color-grey-300); 9 | 10 | &:hover, 11 | &:focus { 12 | border-color: var(--color-grey-300); 13 | } 14 | } 15 | 16 | &_view_success { 17 | background: var(--color-blue-700); 18 | border-color: var(--color-blue-700); 19 | color: var(--color-white); 20 | 21 | &:hover, 22 | &:focus { 23 | background: var(--color-blue-600); 24 | } 25 | } 26 | 27 | &_view_danger { 28 | background: var(--color-red-700); 29 | border-color: var(--color-red-700); 30 | color: var(--color-white); 31 | 32 | &:hover, 33 | &:focus { 34 | background: var(--color-red-600); 35 | } 36 | } 37 | } 38 | 39 | .button { 40 | &__content { 41 | &_view_default { 42 | &::before { 43 | background: var(--color-grey-750); 44 | } 45 | } 46 | 47 | &_view_success { 48 | &::before { 49 | background: var(--color-blue-700); 50 | } 51 | } 52 | 53 | &_view_danger { 54 | &::before { 55 | background: var(--color-red-700); 56 | } 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/renderer/ui/button/button_theme_light.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .button_theme_light { 4 | &.button { 5 | &_view_default { 6 | background: var(--color-grey-100); 7 | border-color: var(--color-grey-150); 8 | color: var(--color-grey-800); 9 | 10 | &:hover, 11 | &:focus { 12 | background: var(--color-white); 13 | border-color: var(--color-grey-300); 14 | } 15 | } 16 | 17 | &_view_success { 18 | background: var(--color-blue-700); 19 | border-color: var(--color-blue-700); 20 | color: var(--color-white); 21 | 22 | &:hover, 23 | &:focus { 24 | background: var(--color-blue-600); 25 | } 26 | } 27 | 28 | &_view_danger { 29 | background: var(--color-red-700); 30 | border-color: var(--color-red-700); 31 | color: var(--color-white); 32 | 33 | &:hover, 34 | &:focus { 35 | background: var(--color-red-600); 36 | } 37 | } 38 | } 39 | 40 | .button { 41 | &__content { 42 | &_view_default { 43 | &::before { 44 | background: var(--color-grey-100); 45 | } 46 | } 47 | 48 | &_view_success { 49 | &::before { 50 | background: var(--color-blue-700); 51 | } 52 | } 53 | 54 | &_view_danger { 55 | &::before { 56 | background: var(--color-red-700); 57 | } 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/renderer/ui/button/index.ts: -------------------------------------------------------------------------------- 1 | export { Button, ButtonSize, ButtonView } from './button'; 2 | -------------------------------------------------------------------------------- /src/renderer/ui/checkbox/checkbox.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | @import url('./checkbox_theme_dark.pcss'); 4 | @import url('./checkbox_theme_light.pcss'); 5 | 6 | .checkbox { 7 | font-size: 0; 8 | 9 | &__inner { 10 | align-items: center; 11 | display: flex; 12 | 13 | &_disabled { 14 | pointer-events: none; 15 | } 16 | 17 | &_width_default { 18 | display: inline-flex; 19 | } 20 | } 21 | 22 | &__input { 23 | border: 0; 24 | width: 0; 25 | height: 0; 26 | display: none; 27 | } 28 | 29 | &__check { 30 | box-sizing: border-box; 31 | border: 1px solid; 32 | cursor: pointer; 33 | border-radius: var(--border-radius-100); 34 | display: flex; 35 | align-items: center; 36 | justify-content: center; 37 | position: relative; 38 | flex-shrink: 0; 39 | font-size: initial; 40 | 41 | &_size_s { 42 | width: var(--grid4); 43 | height: var(--grid4); 44 | } 45 | 46 | &_disabled { 47 | &::before { 48 | content: '';; 49 | position: absolute; 50 | top: -1px; 51 | right: -1px; 52 | bottom: -1px; 53 | left: -1px; 54 | border-radius: var(--border-radius-100); 55 | } 56 | } 57 | } 58 | 59 | &__label { 60 | cursor: pointer; 61 | 62 | &_size_s { 63 | margin-left: var(--grid2); 64 | font-size: var(--font-size-200); 65 | } 66 | 67 | &_width_available { 68 | flex: 1; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/renderer/ui/checkbox/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import { SizeProp } from '@fortawesome/fontawesome-svg-core'; 2 | import { faCheck } from '@fortawesome/free-solid-svg-icons'; 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 4 | import classnames from 'classnames'; 5 | import { ChangeEvent, FocusEvent, ReactElement, ReactNode } from 'react'; 6 | 7 | import { useStyles } from 'renderer/lib/theme'; 8 | 9 | import { useIdHook } from 'renderer/hooks'; 10 | 11 | import styles from './checkbox.pcss'; 12 | 13 | export enum CheckboxSize { 14 | S = 's', 15 | M = 'm', 16 | } 17 | 18 | export enum CheckboxWidth { 19 | Default = 'default', 20 | Available = 'available', 21 | } 22 | 23 | interface Props { 24 | id?: string; 25 | className?: string; 26 | size?: CheckboxSize; 27 | width?: CheckboxWidth; 28 | value?: boolean; 29 | label?: ReactNode; 30 | disabled?: boolean; 31 | onChange?: (value: boolean, event: ChangeEvent) => void; 32 | onBlur?: (event: FocusEvent) => void; 33 | } 34 | 35 | const mapSizeToIconSize: Record = { 36 | [CheckboxSize.S]: 'xs', 37 | [CheckboxSize.M]: 'sm', 38 | }; 39 | 40 | export function Checkbox({ 41 | disabled, 42 | className, 43 | size, 44 | width, 45 | value, 46 | label, 47 | onChange, 48 | onBlur, 49 | ...props 50 | }: Props): ReactElement { 51 | const cn = useStyles(styles, 'checkbox'); 52 | const id = useIdHook(props.id); 53 | 54 | function handleChange(event: ChangeEvent): void { 55 | if (!disabled) { 56 | onChange?.(event.target.checked, event); 57 | } 58 | } 59 | 60 | function renderLabel(): ReactNode { 61 | if (!label) { 62 | return null; 63 | } 64 | 65 | return
{label}
; 66 | } 67 | 68 | return ( 69 |
70 | 71 | 72 | 79 |
80 | ); 81 | } 82 | 83 | Checkbox.defaultProps = { 84 | size: CheckboxSize.M, 85 | width: CheckboxWidth.Default, 86 | value: false, 87 | }; 88 | -------------------------------------------------------------------------------- /src/renderer/ui/checkbox/checkbox_theme_dark.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .checkbox_theme_dark { 4 | .checkbox { 5 | &__check { 6 | border-color: var(--color-grey-900); 7 | background: var(--color-grey-300); 8 | 9 | &_disabled { 10 | &::before { 11 | background: var(--color-grey-800); 12 | opacity: var(--opacity-400); 13 | } 14 | } 15 | 16 | &_checked { 17 | border-color: var(--color-blue-700); 18 | background: var(--color-blue-700); 19 | color: var(--color-white); 20 | 21 | &_disabled { 22 | &::before { 23 | background: var(--color-white); 24 | } 25 | } 26 | } 27 | } 28 | 29 | &__label { 30 | color: var(--color-grey-300); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/renderer/ui/checkbox/checkbox_theme_light.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .checkbox_theme_light { 4 | .checkbox { 5 | &__check { 6 | border-color: var(--color-grey-900); 7 | background: var(--color-white); 8 | 9 | &_disabled { 10 | &::before { 11 | background: var(--color-grey-500); 12 | opacity: var(--opacity-400); 13 | } 14 | } 15 | 16 | &_checked { 17 | border-color: var(--color-blue-700); 18 | background: var(--color-blue-700); 19 | color: var(--color-white); 20 | 21 | &_disabled { 22 | &::before { 23 | background: var(--color-white); 24 | } 25 | } 26 | } 27 | } 28 | 29 | &__label { 30 | color: var(--color-grey-800); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/renderer/ui/checkbox/index.ts: -------------------------------------------------------------------------------- 1 | export { Checkbox, CheckboxSize, CheckboxWidth } from './checkbox'; 2 | -------------------------------------------------------------------------------- /src/renderer/ui/formik-field/formik-field.tsx: -------------------------------------------------------------------------------- 1 | import { useField } from 'formik'; 2 | import React, { ChangeEvent, FC, FocusEvent, ReactElement } from 'react'; 3 | 4 | export type ExtendedComponentProps = ComponentProps & { 5 | error?: React.ReactNode; 6 | onChange?: (value: FieldValue, event: ChangeEvent) => void; 7 | onBlur?: (event: FocusEvent) => void; 8 | onClearClick?: () => void; 9 | }; 10 | 11 | interface FormikFieldProps { 12 | name: string; 13 | component: FC; 14 | componentProps: ExtendedComponentProps; 15 | onChange?: (value: FieldValue, event: ChangeEvent) => void; 16 | onBlur?: (event: FocusEvent) => void; 17 | onClearClick?: () => void; 18 | className?: string; 19 | value?: string; 20 | validate?: (value: FieldValue) => string | void | Promise; 21 | showErrorMessage: boolean; 22 | } 23 | 24 | export function FormikField({ 25 | name, 26 | validate, 27 | showErrorMessage, 28 | className, 29 | component, 30 | componentProps, 31 | onChange, 32 | onBlur, 33 | onClearClick, 34 | }: FormikFieldProps): ReactElement { 35 | const [field, meta, helpers] = useField({ 36 | name, 37 | validate, 38 | }); 39 | 40 | const error = meta.touched ? (showErrorMessage ? meta.error : Boolean(meta.error)) : undefined; 41 | 42 | const preparedComponentProps = { 43 | className, 44 | 45 | ...field, 46 | ...componentProps, 47 | 48 | error, 49 | 50 | onChange: (value: FieldValue, event: ChangeEvent) => { 51 | helpers.setValue(value); 52 | 53 | if (componentProps.onChange) { 54 | componentProps.onChange(value, event); 55 | } 56 | 57 | onChange?.(value, event); 58 | }, 59 | 60 | onBlur: (event: FocusEvent) => { 61 | field.onBlur(event); 62 | 63 | if (componentProps.onBlur) { 64 | componentProps.onBlur(event); 65 | } 66 | 67 | onBlur?.(event); 68 | }, 69 | 70 | onClearClick: () => { 71 | if (componentProps.onClearClick) { 72 | componentProps.onClearClick(); 73 | } 74 | 75 | onClearClick?.(); 76 | }, 77 | }; 78 | 79 | return React.createElement(component, preparedComponentProps); 80 | } 81 | 82 | FormikField.defaultProps = { 83 | showErrorMessage: true, 84 | componentProps: {}, 85 | }; 86 | -------------------------------------------------------------------------------- /src/renderer/ui/formik-field/index.ts: -------------------------------------------------------------------------------- 1 | export { FormikField } from './formik-field'; 2 | -------------------------------------------------------------------------------- /src/renderer/ui/heading/heading.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | @import url('./heading_theme_light.pcss'); 4 | @import url('./heading_theme_dark.pcss'); 5 | 6 | .heading { 7 | font-family: var(--font); 8 | line-height: var(--line-height-200); 9 | font-weight: var(--font-weight-600); 10 | margin-top: 0; 11 | margin-bottom: 0; 12 | 13 | &, *, *::before, *::after { 14 | box-sizing: border-box; 15 | } 16 | 17 | &_size_s { 18 | font-size: var(--font-size-200); 19 | 20 | &_view_default { 21 | margin-top: var(--grid3); 22 | margin-bottom: var(--grid3); 23 | } 24 | } 25 | 26 | &_size_m { 27 | font-size: var(--font-size-400); 28 | 29 | @media (--tablet-s) { 30 | font-size: var(--font-size-500); 31 | } 32 | 33 | &_view_default { 34 | margin-top: var(--grid4); 35 | margin-bottom: var(--grid4); 36 | } 37 | } 38 | 39 | &_size_l { 40 | font-size: var(--font-size-500); 41 | 42 | @media (--tablet-s) { 43 | font-size: var(--font-size-700); 44 | } 45 | 46 | &_view_default { 47 | margin-top: var(--grid4); 48 | margin-bottom: var(--grid4); 49 | 50 | @media (--tablet-s) { 51 | margin-top: var(--grid5); 52 | margin-bottom: var(--grid5); 53 | } 54 | } 55 | } 56 | 57 | &_size_xl { 58 | font-size: var(--font-size-700); 59 | 60 | @media (--tablet-s) { 61 | font-size: var(--font-size-1000); 62 | } 63 | 64 | &_view_default { 65 | margin-top: var(--grid8); 66 | margin-bottom: var(--grid2); 67 | 68 | @media (--tablet-s) { 69 | margin-bottom: var(--grid4); 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/renderer/ui/heading/heading.tsx: -------------------------------------------------------------------------------- 1 | import classnames from 'classnames'; 2 | import React, { ReactElement, ReactNode } from 'react'; 3 | 4 | import { useStyles } from 'renderer/lib/theme'; 5 | 6 | import styles from './heading.pcss'; 7 | 8 | type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6; 9 | 10 | export enum HeadingSize { 11 | XS = 'xs', 12 | S = 's', 13 | M = 'm', 14 | L = 'l', 15 | XL = 'xl', 16 | } 17 | 18 | export enum HeadingView { 19 | Default = 'default', 20 | Condensed = 'condensed', 21 | } 22 | 23 | interface Props { 24 | size: HeadingSize; 25 | view: HeadingView; 26 | children?: ReactNode; 27 | className?: string; 28 | } 29 | 30 | const mapSizeToLevel: Record = { 31 | [HeadingSize.XL]: 1, 32 | [HeadingSize.L]: 2, 33 | [HeadingSize.M]: 3, 34 | [HeadingSize.S]: 4, 35 | [HeadingSize.XS]: 5, 36 | }; 37 | 38 | export function Heading({ size, view, children, className }: Props): ReactElement { 39 | const cn = useStyles(styles, 'heading'); 40 | 41 | const headingProps = { 42 | className: classnames( 43 | cn({ 44 | size, 45 | view, 46 | }), 47 | className, 48 | ), 49 | }; 50 | 51 | return React.createElement(`h${mapSizeToLevel[size]}`, headingProps, children); 52 | } 53 | 54 | Heading.defaultProps = { 55 | size: HeadingSize.L, 56 | view: HeadingView.Default, 57 | }; 58 | -------------------------------------------------------------------------------- /src/renderer/ui/heading/heading_theme_dark.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .heading_theme_dark { 4 | color: var(--color-grey-200); 5 | } 6 | -------------------------------------------------------------------------------- /src/renderer/ui/heading/heading_theme_light.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .heading_theme_light { 4 | color: var(--color-grey-800); 5 | } 6 | -------------------------------------------------------------------------------- /src/renderer/ui/heading/index.ts: -------------------------------------------------------------------------------- 1 | export { Heading, HeadingSize, HeadingView } from './heading'; 2 | -------------------------------------------------------------------------------- /src/renderer/ui/input/index.ts: -------------------------------------------------------------------------------- 1 | export { Input, InputSize, InputWidth } from './input'; 2 | export type { InputProps, InputType } from './input'; 3 | -------------------------------------------------------------------------------- /src/renderer/ui/input/input.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | @import url('./input_theme_dark.pcss'); 4 | @import url('./input_theme_light.pcss'); 5 | 6 | .input { 7 | &__label { 8 | display: block; 9 | padding-bottom: var(--grid1); 10 | 11 | &_size_s { 12 | line-height: var(--grid4); 13 | font-size: var(--font-size-200); 14 | } 15 | 16 | &_size_m { 17 | line-height: var(--grid5); 18 | font-size: var(--font-size-400); 19 | } 20 | } 21 | 22 | &__input { 23 | outline: none; 24 | box-sizing: border-box; 25 | padding: 0; 26 | border: 0; 27 | 28 | &_size_s { 29 | line-height: calc(var(--grid6) - 2px); 30 | font-size: var(--font-size-200); 31 | } 32 | 33 | &_size_m { 34 | line-height: calc(var(--grid7) - 2px); 35 | font-size: var(--font-size-400); 36 | } 37 | 38 | &_width_available { 39 | display: block; 40 | width: 100%; 41 | } 42 | } 43 | 44 | &__inner { 45 | display: flex; 46 | border-radius: var(--border-radius-200); 47 | border: 1px solid; 48 | overflow: hidden; 49 | 50 | &_size_s { 51 | padding: 0 var(--grid2); 52 | } 53 | 54 | &_size_m { 55 | padding: 0 var(--grid2); 56 | } 57 | 58 | &_with-right-addon { 59 | padding-right: 0; 60 | } 61 | 62 | &_disabled { 63 | pointer-events: none; 64 | position: relative; 65 | 66 | &::before { 67 | content: ''; 68 | position: absolute; 69 | top: -1px; 70 | right: -1px; 71 | bottom: -1px; 72 | left: -1px; 73 | border-radius: var(--border-radius-200); 74 | } 75 | } 76 | } 77 | 78 | &__right-addon { 79 | padding: 0 var(--grid2); 80 | display: flex; 81 | align-items: center; 82 | justify-content: center; 83 | outline: 0; 84 | 85 | &_with-click { 86 | cursor: pointer; 87 | } 88 | } 89 | 90 | &__hint { 91 | &_size_s { 92 | font-size: var(--font-size-100); 93 | margin-top: var(--grid0_5); 94 | } 95 | 96 | &_size_m { 97 | font-size: var(--font-size-200); 98 | margin-top: var(--grid1); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/renderer/ui/input/input_theme_dark.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .input_theme_dark { 4 | .input { 5 | &__label { 6 | color: var(--color-grey-300); 7 | } 8 | 9 | &__input { 10 | color: var(--color-grey-300); 11 | background: var(--color-grey-850); 12 | } 13 | 14 | &__inner { 15 | border-color: var(--color-grey-900); 16 | background: var(--color-grey-850); 17 | 18 | &_focused { 19 | border-color: var(--color-blue-700); 20 | box-shadow: 0 0 0 1px var(--color-blue-shadow); 21 | } 22 | 23 | &_disabled { 24 | &::before { 25 | background: var(--color-white); 26 | opacity: var(--opacity-400); 27 | } 28 | } 29 | } 30 | 31 | &__right-addon { 32 | color: var(--color-grey-300); 33 | } 34 | 35 | &__hint { 36 | color: var(--color-grey-400); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/renderer/ui/input/input_theme_light.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .input_theme_light { 4 | .input { 5 | &__label { 6 | color: var(--color-grey-800); 7 | } 8 | 9 | &__input { 10 | color: var(--color-grey-800); 11 | background: var(--color-white); 12 | } 13 | 14 | &__inner { 15 | border-color: var(--color-grey-150); 16 | background: var(--color-white); 17 | 18 | &_focused { 19 | border-color: var(--color-blue-700); 20 | box-shadow: 0 0 0 1px var(--color-blue-shadow); 21 | } 22 | 23 | &_disabled { 24 | &::before { 25 | background: var(--color-grey-300); 26 | opacity: var(--opacity-400); 27 | } 28 | } 29 | } 30 | 31 | &__right-addon { 32 | color: var(--color-grey-750); 33 | } 34 | 35 | &__hint { 36 | color: var(--color-grey-700); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/renderer/ui/label/index.ts: -------------------------------------------------------------------------------- 1 | export { Label, LabelSize } from './label'; 2 | -------------------------------------------------------------------------------- /src/renderer/ui/label/label.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | @import url('./label_theme_dark.pcss'); 4 | @import url('./label_theme_light.pcss'); 5 | 6 | .label { 7 | &_size_s { 8 | line-height: var(--grid4); 9 | font-size: var(--font-size-200); 10 | } 11 | 12 | &_size_m { 13 | font-size: var(--font-size-400); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/renderer/ui/label/label.tsx: -------------------------------------------------------------------------------- 1 | import classnames from 'classnames'; 2 | import { ReactElement, ReactNode } from 'react'; 3 | 4 | import { useStyles } from 'renderer/lib/theme'; 5 | 6 | import styles from './label.pcss'; 7 | 8 | export enum LabelSize { 9 | S = 's', 10 | M = 'm', 11 | } 12 | 13 | interface Props { 14 | size: LabelSize; 15 | children?: ReactNode; 16 | className?: string; 17 | } 18 | 19 | export function Label({ size, children, className }: Props): ReactElement { 20 | const cn = useStyles(styles, 'label'); 21 | 22 | return
{children}
; 23 | } 24 | 25 | Label.defaultProps = { size: LabelSize.M }; 26 | -------------------------------------------------------------------------------- /src/renderer/ui/label/label_theme_dark.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .label_theme_dark { 4 | color: var(--color-grey-300); 5 | } 6 | -------------------------------------------------------------------------------- /src/renderer/ui/label/label_theme_light.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .label_theme_light { 4 | color: var(--color-grey-800); 5 | } 6 | -------------------------------------------------------------------------------- /src/renderer/ui/modal/index.ts: -------------------------------------------------------------------------------- 1 | export { Modal } from './modal'; 2 | -------------------------------------------------------------------------------- /src/renderer/ui/modal/modal.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | @import url('./modal_theme_light.pcss'); 4 | @import url('./modal_theme_dark.pcss'); 5 | 6 | .modal { 7 | position: absolute; 8 | width: calc(100% - var(--grid4)); 9 | border-radius: var(--border-radius-200); 10 | top: 50%; 11 | left: 50%; 12 | transform: translate(-50%, -50%); 13 | 14 | &:focus-visible, 15 | &:focus { 16 | outline: 0; 17 | } 18 | 19 | &__portal { 20 | position: relative; 21 | z-index: 2; 22 | } 23 | 24 | &__overlay { 25 | position: fixed; 26 | top: 0; 27 | left: 0; 28 | right: 0; 29 | bottom: 0; 30 | background-color: var(--color-overlay); 31 | opacity: 0; 32 | transition: opacity 200ms ease-in-out; 33 | 34 | &:global(.ReactModal__Overlay--after-open) { 35 | opacity: 1; 36 | } 37 | 38 | &:global(.ReactModal__Overlay--before-close) { 39 | opacity: 0; 40 | } 41 | } 42 | 43 | &__header { 44 | padding: var(--grid4); 45 | border-bottom: 1px solid; 46 | display: flex; 47 | align-items: center; 48 | justify-content: space-between; 49 | } 50 | 51 | &__content { 52 | &_view_default { 53 | padding: var(--grid4) var(--grid2); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/renderer/ui/modal/modal.tsx: -------------------------------------------------------------------------------- 1 | import { faTimes } from '@fortawesome/free-solid-svg-icons'; 2 | import classnames from 'classnames'; 3 | import { ReactElement, ReactNode } from 'react'; 4 | import ReactModal from 'react-modal'; 5 | 6 | import { useStyles } from 'renderer/lib/theme'; 7 | 8 | import { ButtonIcon } from 'renderer/ui/button-icon'; 9 | import { Heading, HeadingSize, HeadingView } from 'renderer/ui/heading'; 10 | 11 | import styles from './modal.pcss'; 12 | 13 | ReactModal.setAppElement('#root'); 14 | 15 | export enum ModalView { 16 | Default = 'default', 17 | Condensed = 'condensed', 18 | } 19 | 20 | interface Props { 21 | children?: ReactNode; 22 | className?: string; 23 | open?: boolean; 24 | onClose?: () => void; 25 | title: string; 26 | view: ModalView; 27 | } 28 | 29 | export function Modal(props: Props): ReactElement { 30 | const { children, open, title, onClose, view, className } = props; 31 | const cn = useStyles(styles, 'modal'); 32 | 33 | return ( 34 | 42 |
43 | 44 | {title} 45 | 46 | 47 | 48 |
49 |
{children}
50 |
51 | ); 52 | } 53 | 54 | Modal.defaultProps = { 55 | view: ModalView.Default, 56 | }; 57 | -------------------------------------------------------------------------------- /src/renderer/ui/modal/modal_theme_dark.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .modal_theme_dark { 4 | background: var(--color-grey-800); 5 | 6 | .modal { 7 | &__header { 8 | border-bottom-color: var(--color-grey-900); 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/renderer/ui/modal/modal_theme_light.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .modal_theme_light { 4 | background: var(--color-white); 5 | 6 | .modal { 7 | &__header { 8 | border-bottom-color: var(--color-grey-150); 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/renderer/ui/number-input/index.ts: -------------------------------------------------------------------------------- 1 | export { InputSize, InputWidth } from 'renderer/ui/input'; 2 | 3 | export { NumberInput } from './number-input'; 4 | -------------------------------------------------------------------------------- /src/renderer/ui/number-input/number-input.tsx: -------------------------------------------------------------------------------- 1 | import { ChangeEvent, ReactElement, useState } from 'react'; 2 | 3 | import { parseNumber } from 'renderer/lib/numbers'; 4 | 5 | import { Input, InputProps } from 'renderer/ui/input'; 6 | 7 | interface Props extends Omit { 8 | step?: number; 9 | minValue?: number; 10 | maxValue?: number; 11 | value?: number; 12 | onChange?: (value: number | undefined, event: ChangeEvent) => void; 13 | } 14 | 15 | export function NumberInput({ step, minValue, maxValue, value, onChange, ...props }: Props): ReactElement { 16 | const [localValue, setLocalValue] = useState(value !== undefined ? value.toString() : ''); 17 | 18 | function isInInterval(val: number): boolean { 19 | return (minValue === undefined || minValue <= val) && (maxValue === undefined || maxValue >= val); 20 | } 21 | 22 | function handleChange(value: string, event: ChangeEvent): void { 23 | if (value === '') { 24 | setLocalValue(value); 25 | onChange?.(undefined, event); 26 | return; 27 | } 28 | 29 | let num = parseNumber(value); 30 | if (value === '-' && (!minValue || minValue < 0)) { 31 | num = 0; 32 | } 33 | 34 | if (Number.isNaN(num) || !isInInterval(num)) { 35 | return; 36 | } 37 | 38 | setLocalValue(value); 39 | onChange?.(num, event); 40 | } 41 | 42 | return ( 43 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/renderer/ui/paragraph/index.ts: -------------------------------------------------------------------------------- 1 | export { Paragraph, ParagraphSize } from './paragraph'; 2 | -------------------------------------------------------------------------------- /src/renderer/ui/paragraph/paragraph.pcss: -------------------------------------------------------------------------------- 1 | @import url('./paragraph_theme_dark.pcss'); 2 | @import url('./paragraph_theme_light.pcss'); 3 | 4 | .paragraph { 5 | margin: 0; 6 | 7 | &_size_s { 8 | font-size: var(--font-size-200); 9 | } 10 | 11 | &_size_m { 12 | font-size: var(--font-size-400); 13 | } 14 | 15 | &_size_l { 16 | font-size: var(--font-size-500); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/renderer/ui/paragraph/paragraph.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement, ReactNode } from 'react'; 2 | 3 | import { useStyles } from 'renderer/lib/theme'; 4 | 5 | import styles from './paragraph.pcss'; 6 | 7 | export enum ParagraphSize { 8 | S = 's', 9 | M = 'm', 10 | L = 'l', 11 | } 12 | 13 | interface Props { 14 | children?: ReactNode; 15 | className?: string; 16 | size: ParagraphSize; 17 | } 18 | 19 | export function Paragraph({ children, className, size }: Props): ReactElement { 20 | const cn = useStyles(styles, 'paragraph', className); 21 | 22 | return

{children}

; 23 | } 24 | 25 | Paragraph.defaultProps = { 26 | size: ParagraphSize.M, 27 | }; 28 | -------------------------------------------------------------------------------- /src/renderer/ui/paragraph/paragraph_theme_dark.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .paragraph_theme_dark { 4 | color: var(--color-grey-200); 5 | } 6 | -------------------------------------------------------------------------------- /src/renderer/ui/paragraph/paragraph_theme_light.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .paragraph_theme_light { 4 | color: var(--color-grey-800); 5 | } 6 | -------------------------------------------------------------------------------- /src/renderer/ui/password-input/index.ts: -------------------------------------------------------------------------------- 1 | export { InputSize, InputWidth } from 'renderer/ui/input'; 2 | 3 | export { PasswordInput } from './password-input'; 4 | -------------------------------------------------------------------------------- /src/renderer/ui/password-input/password-input.tsx: -------------------------------------------------------------------------------- 1 | import { SizeProp } from '@fortawesome/fontawesome-svg-core'; 2 | import { faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons'; 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 4 | import { ChangeEvent, FocusEvent, ReactElement, useState } from 'react'; 5 | 6 | import { Input, InputProps, InputSize, InputType } from 'renderer/ui/input'; 7 | 8 | type Props = Omit; 9 | 10 | const mapSizeToIconSize: Record = { 11 | [InputSize.S]: 'xs', 12 | [InputSize.M]: 'sm', 13 | }; 14 | 15 | export function PasswordInput({ value, onBlur, onFocus, onChange, ...props }: Props): ReactElement { 16 | const [type, setType] = useState('password'); 17 | const [hasInitialValue, setHasInitialValue] = useState(Boolean(value)); 18 | const [hideValue, setHideValue] = useState(false); 19 | 20 | const isPasswordType = type === 'password'; 21 | 22 | function handleToggleType(): void { 23 | if (isPasswordType) { 24 | setType('text'); 25 | } else { 26 | setType('password'); 27 | } 28 | } 29 | 30 | function handleFocus(event: FocusEvent): void { 31 | if (hasInitialValue) { 32 | setHideValue(true); 33 | } 34 | 35 | onFocus?.(event); 36 | } 37 | 38 | function handleBlur(event: FocusEvent): void { 39 | if (hideValue) { 40 | setHideValue(false); 41 | } 42 | 43 | onBlur?.(event); 44 | } 45 | 46 | function handleChange(newValue: string, event: ChangeEvent): void { 47 | if (hasInitialValue) { 48 | setHasInitialValue(false); 49 | } 50 | 51 | if (hideValue) { 52 | setHideValue(false); 53 | } 54 | 55 | onChange?.(newValue, event); 56 | } 57 | 58 | return ( 59 | 69 | ) 70 | } 71 | onRightAddonClick={handleToggleType} 72 | onFocus={handleFocus} 73 | onBlur={handleBlur} 74 | onChange={handleChange} 75 | /> 76 | ); 77 | } 78 | 79 | PasswordInput.defaultProps = { 80 | size: InputSize.M, 81 | }; 82 | -------------------------------------------------------------------------------- /src/renderer/ui/select/index.ts: -------------------------------------------------------------------------------- 1 | export { Select, SelectSize, SelectWidth } from './select'; 2 | export type { SelectItem } from './select'; 3 | -------------------------------------------------------------------------------- /src/renderer/ui/select/select.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | @import url('./select_theme_dark.pcss'); 4 | @import url('./select_theme_light.pcss'); 5 | 6 | .select { 7 | &__label { 8 | display: block; 9 | padding-bottom: var(--grid1); 10 | 11 | &_size_s { 12 | line-height: var(--grid4); 13 | font-size: var(--font-size-200); 14 | } 15 | 16 | &_size_m { 17 | line-height: var(--grid5); 18 | font-size: var(--font-size-400); 19 | } 20 | } 21 | 22 | &__select { 23 | outline: none; 24 | box-sizing: border-box; 25 | border: 0; 26 | 27 | &_size_s { 28 | height: var(--grid6); 29 | font-size: var(--font-size-200); 30 | } 31 | 32 | &_size_m { 33 | height: var(--grid7); 34 | font-size: var(--font-size-400); 35 | } 36 | 37 | &_width_available { 38 | display: block; 39 | width: 100%; 40 | } 41 | } 42 | 43 | &__select-wrap { 44 | border-radius: var(--border-radius-200); 45 | position: relative; 46 | border: 1px solid; 47 | 48 | &_disabled { 49 | pointer-events: none; 50 | 51 | &::before { 52 | content: ''; 53 | position: absolute; 54 | top: 0; 55 | right: 0; 56 | bottom: 0; 57 | left: 0; 58 | z-index: 1; 59 | border-radius: var(--border-radius-200); 60 | } 61 | } 62 | 63 | &_size_s { 64 | padding: 0 var(--grid2); 65 | } 66 | 67 | &_size_m { 68 | padding: 0 var(--grid2); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/renderer/ui/select/select_theme_dark.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .select_theme_dark { 4 | .select { 5 | &__label { 6 | color: var(--color-grey-300); 7 | } 8 | 9 | &__select { 10 | color: var(--color-grey-300); 11 | background: var(--color-grey-850); 12 | } 13 | 14 | &__select-wrap { 15 | border-color: var(--color-grey-900); 16 | background: var(--color-grey-850); 17 | 18 | &_disabled { 19 | &::before { 20 | background: var(--color-white); 21 | opacity: var(--opacity-400); 22 | } 23 | } 24 | 25 | &_focused { 26 | border-color: var(--color-blue-700); 27 | box-shadow: 0 0 0 1px var(--color-blue-shadow); 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/renderer/ui/select/select_theme_light.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .select_theme_light { 4 | .select { 5 | &__label { 6 | color: var(--color-grey-800); 7 | } 8 | 9 | &__select { 10 | color: var(--color-grey-800); 11 | background: var(--color-white); 12 | } 13 | 14 | &__select-wrap { 15 | border-color: var(--color-grey-150); 16 | background: var(--color-white); 17 | 18 | &_disabled { 19 | &::before { 20 | background: var(--color-grey-300); 21 | opacity: var(--opacity-400); 22 | } 23 | } 24 | 25 | &_focused { 26 | border-color: var(--color-blue-700); 27 | box-shadow: 0 0 0 1px var(--color-blue-shadow); 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/renderer/ui/spinner/index.ts: -------------------------------------------------------------------------------- 1 | export { Spinner, SpinnerView, SpinnerSize } from './spinner'; 2 | -------------------------------------------------------------------------------- /src/renderer/ui/spinner/spinner.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | @import url('./spinner_theme_dark.pcss'); 4 | @import url('./spinner_theme_light.pcss'); 5 | 6 | .spinner { 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | 11 | &_view_block { 12 | height: 100%; 13 | min-height: calc(var(--grid5) * 10); 14 | } 15 | 16 | &__loader { 17 | display: inline-block; 18 | position: relative; 19 | 20 | &_size_xs { 21 | width: var(--grid2_5); 22 | height: var(--grid4); 23 | } 24 | 25 | &_size_s { 26 | width: var(--grid5); 27 | height: var(--grid6); 28 | } 29 | 30 | &_size_m { 31 | width: var(--grid10); 32 | height: var(--grid20); 33 | } 34 | } 35 | 36 | &__item { 37 | display: inline-block; 38 | position: absolute; 39 | 40 | &_size_xs { 41 | width: var(--grid0_5); 42 | animation: lds-xs 1.2s cubic-bezier(0, 0.5, 0.5, 1) infinite; 43 | 44 | &:nth-child(2) { 45 | left: var(--grid1); 46 | } 47 | 48 | &:nth-child(3) { 49 | left: var(--grid2); 50 | } 51 | } 52 | 53 | &_size_s { 54 | width: var(--grid1); 55 | animation: lds-s 1.2s cubic-bezier(0, 0.5, 0.5, 1) infinite; 56 | left: 0; 57 | 58 | &:nth-child(2) { 59 | left: var(--grid2); 60 | } 61 | 62 | &:nth-child(3) { 63 | left: var(--grid4); 64 | } 65 | } 66 | 67 | &_size_m { 68 | width: var(--grid2); 69 | animation: lds-m 1.2s cubic-bezier(0, 0.5, 0.5, 1) infinite; 70 | 71 | &:nth-child(2) { 72 | left: var(--grid4); 73 | } 74 | 75 | &:nth-child(3) { 76 | left: var(--grid8); 77 | } 78 | } 79 | 80 | &:nth-child(1) { 81 | animation-delay: -0.24s; 82 | } 83 | 84 | &:nth-child(2) { 85 | animation-delay: -0.12s; 86 | } 87 | 88 | &:nth-child(3) { 89 | animation-delay: 0; 90 | } 91 | } 92 | } 93 | 94 | @keyframes lds-xs { 95 | 0% { 96 | top: 0; 97 | height: var(--grid4); 98 | } 99 | 100 | 50%, 100% { 101 | top: var(--grid1); 102 | height: var(--grid2); 103 | } 104 | } 105 | 106 | @keyframes lds-s { 107 | 0% { 108 | top: 0; 109 | height: var(--grid6); 110 | } 111 | 112 | 50%, 100% { 113 | top: var(--grid1); 114 | height: var(--grid4); 115 | } 116 | } 117 | 118 | @keyframes lds-m { 119 | 0% { 120 | top: var(--grid2); 121 | height: var(--grid16); 122 | } 123 | 124 | 50%, 100% { 125 | top: var(--grid6); 126 | height: var(--grid8); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/renderer/ui/spinner/spinner.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | 3 | import { useStyles } from 'renderer/lib/theme'; 4 | 5 | import styles from './spinner.pcss'; 6 | 7 | export enum SpinnerView { 8 | Default = 'default', 9 | Block = 'block', 10 | } 11 | 12 | export enum SpinnerSize { 13 | XS = 'xs', 14 | S = 's', 15 | M = 'm', 16 | } 17 | 18 | interface Props { 19 | view: SpinnerView; 20 | size: SpinnerSize; 21 | className?: string; 22 | } 23 | 24 | export function Spinner({ view, size, className }: Props): ReactElement { 25 | const cn = useStyles(styles, 'spinner', className); 26 | 27 | return ( 28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | ); 36 | } 37 | 38 | Spinner.defaultProps = { 39 | view: SpinnerView.Default, 40 | size: SpinnerSize.M, 41 | }; 42 | -------------------------------------------------------------------------------- /src/renderer/ui/spinner/spinner_theme_dark.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .spinner_theme_dark { 4 | .spinner { 5 | &__item { 6 | background: var(--color-white); 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/renderer/ui/spinner/spinner_theme_light.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .spinner_theme_light { 4 | .spinner { 5 | &__item { 6 | background: var(--color-blue-700); 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/renderer/ui/table/index.ts: -------------------------------------------------------------------------------- 1 | export { Table, TableSize } from './table'; 2 | -------------------------------------------------------------------------------- /src/renderer/ui/table/table.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | @import url('./table_theme_dark.pcss'); 4 | @import url('./table_theme_light.pcss'); 5 | 6 | .table { 7 | &__rows { 8 | display: grid; 9 | } 10 | 11 | &__column, 12 | &__heading { 13 | font-size: var(--font-size-500); 14 | padding: var(--grid2); 15 | outline: 0; 16 | 17 | &_size_s { 18 | font-size: var(--font-size-200); 19 | } 20 | 21 | &_size_m { 22 | font-size: var(--font-size-400); 23 | } 24 | } 25 | 26 | &__column { 27 | border-top: 1px solid; 28 | 29 | &_selectable { 30 | cursor: pointer; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/renderer/ui/table/table_theme_dark.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .table_theme_dark { 4 | .table { 5 | &__column { 6 | color: var(--color-grey-400); 7 | border-top-color: var(--color-grey-500); 8 | 9 | &_hovered, 10 | &_active { 11 | background: var(--color-grey-750); 12 | } 13 | } 14 | 15 | &__heading { 16 | color: var(--color-grey-300); 17 | background: var(--color-grey-850); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/renderer/ui/table/table_theme_light.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .table_theme_light { 4 | .table { 5 | &__column { 6 | color: var(--color-grey-800); 7 | border-top-color: var(--color-grey-400); 8 | 9 | &_hovered, 10 | &_active { 11 | background: var(--color-grey-100); 12 | } 13 | } 14 | 15 | &__heading { 16 | color: var(--color-grey-900); 17 | background: var(--color-grey-300); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/renderer/ui/tabs/index.ts: -------------------------------------------------------------------------------- 1 | export { Tabs } from './tabs'; 2 | export type { TabItem } from './tabs'; 3 | -------------------------------------------------------------------------------- /src/renderer/ui/tabs/tabs.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | @import url('./tabs_theme_dark.pcss'); 4 | @import url('./tabs_theme_light.pcss'); 5 | 6 | .tabs { 7 | display: flex; 8 | margin-left: var(--grid0_5-neg); 9 | margin-right: var(--grid0_5-neg); 10 | 11 | &__item { 12 | padding: var(--grid2) var(--grid3); 13 | position: relative; 14 | cursor: pointer; 15 | margin: 0 var(--grid0_5); 16 | outline: 0; 17 | border-radius: var(--border-radius-200); 18 | font-size: var(--font-size-200); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/renderer/ui/tabs/tabs.tsx: -------------------------------------------------------------------------------- 1 | import classnames from 'classnames'; 2 | import { ReactElement, ReactNode } from 'react'; 3 | 4 | import { handleEnterEvent } from 'renderer/lib/keyboard'; 5 | import { useStyles } from 'renderer/lib/theme'; 6 | 7 | import styles from './tabs.pcss'; 8 | 9 | export interface TabItem { 10 | value: V; 11 | text: ReactNode; 12 | } 13 | 14 | interface Props { 15 | items: TabItem[]; 16 | active: V; 17 | className?: string; 18 | onChange?: (value: V) => void; 19 | } 20 | 21 | export function Tabs({ items, active, onChange, className }: Props): ReactElement { 22 | const cn = useStyles(styles, 'tabs'); 23 | 24 | function handleItemClick(item: TabItem) { 25 | return () => { 26 | onChange?.(item.value); 27 | }; 28 | } 29 | 30 | function renderItem(item: TabItem): ReactNode { 31 | return ( 32 |
40 | {item.text} 41 |
42 | ); 43 | } 44 | 45 | return
{items.map(renderItem)}
; 46 | } 47 | -------------------------------------------------------------------------------- /src/renderer/ui/tabs/tabs_theme_dark.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .tabs_theme_dark { 4 | .tabs { 5 | &__item { 6 | color: var(--color-grey-400); 7 | 8 | &:hover { 9 | background: var(--color-grey-750); 10 | } 11 | 12 | &_active { 13 | background: var(--color-blue-700); 14 | color: var(--color-white); 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/renderer/ui/tabs/tabs_theme_light.pcss: -------------------------------------------------------------------------------- 1 | @import url('variables.pcss'); 2 | 3 | .tabs_theme_light { 4 | .tabs { 5 | &__item { 6 | color: var(--color-grey-800); 7 | 8 | &:hover { 9 | background: var(--color-blue-50); 10 | } 11 | 12 | &_active { 13 | background: var(--color-blue-700); 14 | color: var(--color-white); 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/renderer/ui/textarea/index.ts: -------------------------------------------------------------------------------- 1 | export { Textarea, TextareaSize, TextareaWidth } from './textarea'; 2 | -------------------------------------------------------------------------------- /src/renderer/ui/textarea/textarea.pcss: -------------------------------------------------------------------------------- 1 | @import url('./textarea_theme_dark.pcss'); 2 | @import url('./textarea_theme_light.pcss'); 3 | 4 | .textarea { 5 | &_max-height { 6 | display: flex; 7 | flex-direction: column; 8 | } 9 | 10 | &__label { 11 | display: block; 12 | padding-bottom: var(--grid1); 13 | 14 | &_size_s { 15 | line-height: var(--grid4); 16 | font-size: var(--font-size-200); 17 | } 18 | 19 | &_size_m { 20 | line-height: var(--grid5); 21 | font-size: var(--font-size-400); 22 | } 23 | } 24 | 25 | &__textarea { 26 | resize: none; 27 | border: 0; 28 | outline: none; 29 | 30 | &_size_s { 31 | font-size: var(--font-size-200); 32 | } 33 | 34 | &_size_m { 35 | font-size: var(--font-size-400); 36 | } 37 | 38 | &_width_available { 39 | display: block; 40 | width: 100%; 41 | } 42 | 43 | &_resizable { 44 | resize: both; 45 | } 46 | 47 | &_max-height { 48 | height: 100%; 49 | } 50 | } 51 | 52 | &__inner { 53 | display: flex; 54 | border-radius: var(--border-radius-200); 55 | border: 1px solid; 56 | overflow: hidden; 57 | 58 | &_size_s { 59 | padding: 0 var(--grid2); 60 | } 61 | 62 | &_size_m { 63 | padding: 0 var(--grid2); 64 | } 65 | 66 | &_disabled { 67 | pointer-events: none; 68 | position: relative; 69 | 70 | &::before { 71 | content: ''; 72 | position: absolute; 73 | top: -1px; 74 | right: -1px; 75 | bottom: -1px; 76 | left: -1px; 77 | border-radius: var(--border-radius-200); 78 | } 79 | } 80 | 81 | &_max-height { 82 | flex: 1; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/renderer/ui/textarea/textarea.tsx: -------------------------------------------------------------------------------- 1 | import classnames from 'classnames'; 2 | import { ChangeEvent, FocusEvent, ReactElement, ReactNode, useState } from 'react'; 3 | 4 | import { useStyles } from 'renderer/lib/theme'; 5 | 6 | import { useIdHook } from 'renderer/hooks'; 7 | 8 | import styles from './textarea.pcss'; 9 | 10 | export enum TextareaSize { 11 | S = 's', 12 | M = 'm', 13 | } 14 | 15 | export enum TextareaWidth { 16 | Default = 'default', 17 | Available = 'available', 18 | } 19 | 20 | interface Props { 21 | value?: string; 22 | onChange?: (value: string, event: ChangeEvent) => void; 23 | onBlur?: (event: FocusEvent) => void; 24 | className?: string; 25 | label?: string; 26 | size?: TextareaSize; 27 | id?: string; 28 | width?: TextareaWidth; 29 | resizable?: boolean; 30 | disabled?: boolean; 31 | maxHeight?: boolean; 32 | } 33 | 34 | export function Textarea({ 35 | className, 36 | value, 37 | label, 38 | size, 39 | width, 40 | disabled, 41 | maxHeight, 42 | onChange, 43 | onBlur, 44 | ...props 45 | }: Props): ReactElement { 46 | const cn = useStyles(styles, 'textarea'); 47 | const id = useIdHook(props.id); 48 | const [focused, setFocused] = useState(false); 49 | 50 | function handleChange(event: ChangeEvent): void { 51 | const value = event.target.value; 52 | onChange?.(value, event); 53 | } 54 | 55 | function handleFocus(): void { 56 | setFocused(true); 57 | } 58 | 59 | function handleBlur(event: FocusEvent): void { 60 | setFocused(false); 61 | onBlur?.(event); 62 | } 63 | 64 | function renderLabel(): ReactNode { 65 | if (!label) { 66 | return null; 67 | } 68 | 69 | return ( 70 | 73 | ); 74 | } 75 | 76 | function renderTextarea(): ReactNode { 77 | return ( 78 |