├── src ├── app │ ├── pages │ │ ├── index.ts │ │ ├── settings │ │ │ ├── index.ts │ │ │ ├── forms │ │ │ │ ├── index.ts │ │ │ │ └── settings.form.tsx │ │ │ └── modals │ │ │ │ ├── index.ts │ │ │ │ └── update-settings.modal.tsx │ │ ├── side-bar │ │ │ ├── index.ts │ │ │ ├── nodes │ │ │ │ ├── index.ts │ │ │ │ ├── node.styled.ts │ │ │ │ ├── service.node.tsx │ │ │ │ └── method.node.tsx │ │ │ ├── collections-tree.styled.ts │ │ │ └── collections-tree.tsx │ │ ├── collections │ │ │ ├── index.ts │ │ │ ├── badge-types │ │ │ │ ├── index.ts │ │ │ │ └── grpc │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── proto.badge.tsx │ │ │ │ │ ├── unary.badge.tsx │ │ │ │ │ ├── service.badge.tsx │ │ │ │ │ └── stream.badge.tsx │ │ │ ├── forms │ │ │ │ ├── index.ts │ │ │ │ ├── fields │ │ │ │ │ ├── index.ts │ │ │ │ │ └── include-directories │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── directory.node.tsx │ │ │ │ └── collection.form.tsx │ │ │ └── modals │ │ │ │ ├── index.ts │ │ │ │ ├── update-collection.modal.tsx │ │ │ │ └── create-collection.modal.tsx │ │ ├── shortcuts │ │ │ ├── index.ts │ │ │ ├── hooks │ │ │ │ ├── index.ts │ │ │ │ ├── use-theme-actions.tsx │ │ │ │ └── use-environment-actions.tsx │ │ │ └── shortcuts.tsx │ │ ├── tabs-container │ │ │ ├── index.ts │ │ │ ├── collection-types │ │ │ │ ├── index.ts │ │ │ │ └── grpc │ │ │ │ │ ├── request │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── tls │ │ │ │ │ ├── index.ts │ │ │ │ │ └── system.badge.tsx │ │ │ │ │ ├── environments │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── create-environment.modal.tsx │ │ │ │ │ └── environment.form.tsx │ │ │ │ │ ├── response │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── streams │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── response.styled.ts │ │ │ │ │ │ ├── client-streaming.response.tsx │ │ │ │ │ │ ├── server-streaming.response.tsx │ │ │ │ │ │ ├── bidirectional-streaming.response.tsx │ │ │ │ │ │ └── stream-icons.tsx │ │ │ │ │ ├── empty-response-view.tsx │ │ │ │ │ └── unary-call.response.tsx │ │ │ │ │ └── send-header │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── send-header.unary-call.tsx │ │ │ │ │ ├── protocol-switch.tsx │ │ │ │ │ └── send-header.server-streaming.tsx │ │ │ ├── welcome │ │ │ │ ├── index.ts │ │ │ │ └── welcome-container.tsx │ │ │ └── tabs-container.tsx │ │ ├── logs │ │ │ ├── index.ts │ │ │ ├── logs.button.tsx │ │ │ └── logs.modal.tsx │ │ ├── main.tsx │ │ └── status-bar.tsx │ ├── components │ │ ├── kbd │ │ │ ├── index.ts │ │ │ └── kbd.ts │ │ ├── badge │ │ │ └── index.ts │ │ ├── kbar │ │ │ ├── index.ts │ │ │ └── kbar.styled.ts │ │ ├── buttons │ │ │ ├── index.ts │ │ │ └── ezy.button.tsx │ │ ├── file-input │ │ │ ├── index.ts │ │ │ └── file.input.tsx │ │ ├── info-label │ │ │ ├── index.ts │ │ │ └── info-label.tsx │ │ ├── code-editor │ │ │ ├── index.ts │ │ │ ├── themes │ │ │ │ └── theme.ts │ │ │ ├── code-editor.styled.ts │ │ │ └── code-editor.tsx │ │ ├── color-circle │ │ │ ├── index.ts │ │ │ └── color-circle.tsx │ │ ├── resizable-panel │ │ │ └── index.ts │ │ ├── notification │ │ │ ├── hooks │ │ │ │ ├── index.ts │ │ │ │ └── use-notification.tsx │ │ │ ├── index.ts │ │ │ ├── notification-container.tsx │ │ │ └── notification.tsx │ │ ├── tabs │ │ │ ├── index.ts │ │ │ ├── hooks │ │ │ │ ├── index.ts │ │ │ │ ├── use-refs.ts │ │ │ │ └── use-on-screen.tsx │ │ │ ├── tab-bar │ │ │ │ ├── index.ts │ │ │ │ ├── tab-bar-item │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── tab-bar-item.tsx │ │ │ │ │ ├── tab-bar-item-draggable.tsx │ │ │ │ │ └── tab-bar-item.styled.ts │ │ │ │ ├── tab-bar.styled.ts │ │ │ │ └── active-bar.tsx │ │ │ ├── tab.tsx │ │ │ ├── tab-content.tsx │ │ │ └── tabs.tsx │ │ ├── menu │ │ │ ├── index.ts │ │ │ ├── menu-item.tsx │ │ │ └── menu.tsx │ │ ├── tree │ │ │ ├── index.ts │ │ │ ├── collapse.button.tsx │ │ │ └── tree.tsx │ │ ├── select │ │ │ ├── index.ts │ │ │ ├── colored │ │ │ │ ├── interfaces │ │ │ │ │ ├── index.ts │ │ │ │ │ └── colored-select-option.interface.ts │ │ │ │ ├── index.ts │ │ │ │ ├── colored-select.tsx │ │ │ │ └── colored-single-value.tsx │ │ │ └── select.tsx │ │ ├── color-picker │ │ │ ├── index.ts │ │ │ ├── color-picker.tsx │ │ │ └── color-picker.input.tsx │ │ ├── icons │ │ │ ├── index.ts │ │ │ ├── ezy.icon.tsx │ │ │ ├── horizontal-layout.icon.tsx │ │ │ └── vertical-layout.icon.tsx │ │ └── index.ts │ ├── layouts │ │ ├── index.ts │ │ └── default.tsx │ ├── context │ │ ├── index.ts │ │ └── app.context.ts │ ├── hooks │ │ ├── protocols │ │ │ ├── index.ts │ │ │ └── grpc │ │ │ │ ├── index.ts │ │ │ │ ├── prepare-request.ts │ │ │ │ └── use-unary-call.ts │ │ ├── collections │ │ │ ├── index.ts │ │ │ ├── use-create-collection.ts │ │ │ └── use-update-collection.ts │ │ ├── index.ts │ │ └── use-shortcuts.ts │ ├── themes │ │ ├── index.ts │ │ ├── light.ts │ │ ├── dark.ts │ │ └── global.css.ts │ ├── storage │ │ ├── interfaces │ │ │ ├── tabs │ │ │ │ ├── index.ts │ │ │ │ ├── grpc-tab.interface.ts │ │ │ │ └── tabs.interface.ts │ │ │ ├── index.ts │ │ │ ├── logs.interface.ts │ │ │ ├── environments.interface.ts │ │ │ ├── tls-presets.interface.ts │ │ │ ├── settings.interface.ts │ │ │ └── collections.interface.ts │ │ ├── index.ts │ │ ├── environments.storage.ts │ │ ├── logs.storage.ts │ │ ├── settings.storage.ts │ │ └── tls-presets.storage.ts │ ├── index.html │ ├── renderer.tsx │ └── app.tsx ├── core │ ├── clients │ │ ├── index.ts │ │ └── grpc │ │ │ ├── grpc-web-client │ │ │ ├── interfaces │ │ │ │ ├── index.ts │ │ │ │ └── metadata.interface.ts │ │ │ ├── index.ts │ │ │ ├── metadata-parser.ts │ │ │ ├── grpc-web.error.ts │ │ │ └── grpc-web-call.stream.ts │ │ │ ├── grpc-client │ │ │ ├── index.ts │ │ │ ├── metadata-parser.ts │ │ │ └── __tests__ │ │ │ │ └── metadata-parser.spec.ts │ │ │ ├── index.ts │ │ │ └── interfaces │ │ │ ├── index.ts │ │ │ ├── response.interface.ts │ │ │ ├── status.enum.ts │ │ │ └── client.interface.ts │ ├── index.ts │ ├── protobuf │ │ ├── interfaces │ │ │ ├── index.ts │ │ │ └── protobuf-loader.interface.ts │ │ └── index.ts │ ├── typings.ts │ └── __tests__ │ │ └── fixtures │ │ └── proto │ │ ├── basic.proto │ │ └── simple.proto ├── main │ ├── common │ │ ├── index.ts │ │ └── parse-error.ts │ ├── os │ │ ├── constants.ts │ │ ├── index.ts │ │ ├── preload.ts │ │ └── subscribers.ts │ ├── dialog │ │ ├── index.ts │ │ ├── constants.ts │ │ ├── preload.ts │ │ └── subscribers.ts │ ├── protobuf │ │ ├── index.ts │ │ ├── constants.ts │ │ ├── subscribers.ts │ │ └── preload.ts │ ├── electron-store │ │ ├── index.ts │ │ ├── constants.ts │ │ ├── subscribers.ts │ │ └── preload.ts │ ├── clients │ │ ├── grpc-client │ │ │ ├── index.ts │ │ │ ├── preload │ │ │ │ ├── index.ts │ │ │ │ ├── handlers.ts │ │ │ │ └── unary.ts │ │ │ ├── subscribers │ │ │ │ ├── unary.subscriber.ts │ │ │ │ └── index.ts │ │ │ └── constants.ts │ │ ├── index.ts │ │ └── grpc-web-client │ │ │ ├── index.ts │ │ │ ├── preload │ │ │ ├── index.ts │ │ │ ├── handlers.ts │ │ │ └── unary.ts │ │ │ ├── constants.ts │ │ │ └── subscribers │ │ │ ├── index.ts │ │ │ └── unary.subscriber.ts │ └── preload.ts └── typings.d.ts ├── .ncurc.js ├── docs ├── logo.png └── preview.gif ├── __tests__ ├── simple-service │ ├── .eslintignore │ ├── README.md │ ├── docker-compose.yaml │ ├── tsconfig.json │ ├── package.json │ ├── .eslintrc.js │ ├── proto │ │ └── simple.proto │ └── src │ │ └── generated │ │ └── google │ │ └── protobuf │ │ └── empty.ts ├── tls-service │ ├── .eslintignore │ ├── .gitignore │ ├── proto │ │ └── tls_service.proto │ ├── tsconfig.json │ ├── docker-compose.yaml │ ├── .eslintrc.js │ ├── README.md │ ├── package.json │ ├── src │ │ ├── client.ts │ │ └── index.ts │ └── certs │ │ └── gen-certs.sh └── basic-service │ ├── proto │ ├── basic.proto │ └── basic_grpc_pb.js │ ├── package.json │ └── index.js ├── assets └── icons │ ├── icon.ico │ ├── icon.png │ └── icon.icns ├── commitlint.config.js ├── .husky ├── pre-commit └── commit-msg ├── scripts ├── gen-icons.sh └── add-osx-cert.sh ├── webpack.plugins.js ├── .storybook ├── manager.js ├── main.js └── preview.js ├── .eslintignore ├── wallaby.js ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── ---feature-request.md │ └── ---bug-report.md └── workflows │ └── checks.yml ├── .release-it.json ├── static └── entitlements.plist ├── .vscode └── settings.json ├── webpack.main.config.js ├── webpack.rules.js ├── .gitignore ├── webpack.renderer.config.js ├── tsconfig.json ├── .eslintrc.js └── stories ├── Select.stories.tsx └── ColoredSelect.stories.tsx /src/app/pages/index.ts: -------------------------------------------------------------------------------- 1 | export * from './main'; 2 | -------------------------------------------------------------------------------- /src/core/clients/index.ts: -------------------------------------------------------------------------------- 1 | export * from './grpc'; 2 | -------------------------------------------------------------------------------- /src/app/components/kbd/index.ts: -------------------------------------------------------------------------------- 1 | export * from './kbd'; 2 | -------------------------------------------------------------------------------- /src/app/layouts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './default'; 2 | -------------------------------------------------------------------------------- /.ncurc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reject: [ 3 | ] 4 | }; 5 | -------------------------------------------------------------------------------- /src/app/components/badge/index.ts: -------------------------------------------------------------------------------- 1 | export * from './badge'; 2 | -------------------------------------------------------------------------------- /src/app/components/kbar/index.ts: -------------------------------------------------------------------------------- 1 | export * from './kbar'; 2 | -------------------------------------------------------------------------------- /src/app/context/index.ts: -------------------------------------------------------------------------------- 1 | export * from './app.context'; 2 | -------------------------------------------------------------------------------- /src/app/hooks/protocols/index.ts: -------------------------------------------------------------------------------- 1 | export * from './grpc'; 2 | -------------------------------------------------------------------------------- /src/app/pages/settings/index.ts: -------------------------------------------------------------------------------- 1 | export * from './modals'; 2 | -------------------------------------------------------------------------------- /src/app/pages/side-bar/index.ts: -------------------------------------------------------------------------------- 1 | export * from './side-bar'; 2 | -------------------------------------------------------------------------------- /src/main/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './parse-error'; 2 | -------------------------------------------------------------------------------- /src/app/pages/collections/index.ts: -------------------------------------------------------------------------------- 1 | export * from './modals'; 2 | -------------------------------------------------------------------------------- /src/app/pages/shortcuts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './shortcuts'; 2 | -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getezy/ezy/HEAD/docs/logo.png -------------------------------------------------------------------------------- /src/app/components/buttons/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ezy.button'; 2 | -------------------------------------------------------------------------------- /src/app/components/file-input/index.ts: -------------------------------------------------------------------------------- 1 | export * from './file.input'; 2 | -------------------------------------------------------------------------------- /src/app/components/info-label/index.ts: -------------------------------------------------------------------------------- 1 | export * from './info-label'; 2 | -------------------------------------------------------------------------------- /__tests__/simple-service/.eslintignore: -------------------------------------------------------------------------------- 1 | .eslintrc.js 2 | node_modules/**/* 3 | -------------------------------------------------------------------------------- /__tests__/tls-service/.eslintignore: -------------------------------------------------------------------------------- 1 | .eslintrc.js 2 | node_modules/**/* 3 | -------------------------------------------------------------------------------- /docs/preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getezy/ezy/HEAD/docs/preview.gif -------------------------------------------------------------------------------- /src/app/components/code-editor/index.ts: -------------------------------------------------------------------------------- 1 | export * from './code-editor'; 2 | -------------------------------------------------------------------------------- /src/app/components/color-circle/index.ts: -------------------------------------------------------------------------------- 1 | export * from './color-circle'; 2 | -------------------------------------------------------------------------------- /src/app/pages/collections/badge-types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './grpc'; 2 | -------------------------------------------------------------------------------- /src/app/pages/settings/forms/index.ts: -------------------------------------------------------------------------------- 1 | export * from './settings.form'; 2 | -------------------------------------------------------------------------------- /src/app/pages/tabs-container/index.ts: -------------------------------------------------------------------------------- 1 | export * from './tabs-container'; 2 | -------------------------------------------------------------------------------- /__tests__/tls-service/.gitignore: -------------------------------------------------------------------------------- 1 | certs/*.pem 2 | certs/*.srl 3 | certs/*.cnf 4 | -------------------------------------------------------------------------------- /src/app/components/resizable-panel/index.ts: -------------------------------------------------------------------------------- 1 | export * from './resizable-panel'; 2 | -------------------------------------------------------------------------------- /src/app/pages/collections/forms/index.ts: -------------------------------------------------------------------------------- 1 | export * from './collection.form'; 2 | -------------------------------------------------------------------------------- /src/app/pages/settings/modals/index.ts: -------------------------------------------------------------------------------- 1 | export * from './update-settings.modal'; 2 | -------------------------------------------------------------------------------- /src/app/pages/tabs-container/collection-types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './grpc'; 2 | -------------------------------------------------------------------------------- /src/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './clients'; 2 | export * from './protobuf'; 3 | -------------------------------------------------------------------------------- /src/main/os/constants.ts: -------------------------------------------------------------------------------- 1 | export enum OSChannel { 2 | GET = 'os:get', 3 | } 4 | -------------------------------------------------------------------------------- /assets/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getezy/ezy/HEAD/assets/icons/icon.ico -------------------------------------------------------------------------------- /assets/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getezy/ezy/HEAD/assets/icons/icon.png -------------------------------------------------------------------------------- /src/app/components/notification/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-notification'; 2 | -------------------------------------------------------------------------------- /src/app/components/tabs/index.ts: -------------------------------------------------------------------------------- 1 | export * from './tabs'; 2 | export * from './tab'; 3 | -------------------------------------------------------------------------------- /src/app/pages/tabs-container/welcome/index.ts: -------------------------------------------------------------------------------- 1 | export * from './welcome-container'; 2 | -------------------------------------------------------------------------------- /src/core/protobuf/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './protobuf-loader.interface'; 2 | -------------------------------------------------------------------------------- /src/main/os/index.ts: -------------------------------------------------------------------------------- 1 | export * from './preload'; 2 | export * from './subscribers'; 3 | -------------------------------------------------------------------------------- /assets/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getezy/ezy/HEAD/assets/icons/icon.icns -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /src/app/components/menu/index.ts: -------------------------------------------------------------------------------- 1 | export * from './menu'; 2 | export * from './menu-item'; 3 | -------------------------------------------------------------------------------- /src/app/components/tree/index.ts: -------------------------------------------------------------------------------- 1 | export * from './tree'; 2 | export * from './tree-node'; 3 | -------------------------------------------------------------------------------- /src/app/pages/collections/forms/fields/index.ts: -------------------------------------------------------------------------------- 1 | export * from './include-directories'; 2 | -------------------------------------------------------------------------------- /src/main/dialog/index.ts: -------------------------------------------------------------------------------- 1 | export * from './preload'; 2 | export * from './subscribers'; 3 | -------------------------------------------------------------------------------- /src/main/protobuf/index.ts: -------------------------------------------------------------------------------- 1 | export * from './preload'; 2 | export * from './subscribers'; 3 | -------------------------------------------------------------------------------- /src/app/components/select/index.ts: -------------------------------------------------------------------------------- 1 | export * from './select'; 2 | export * from './colored'; 3 | -------------------------------------------------------------------------------- /src/app/pages/logs/index.ts: -------------------------------------------------------------------------------- 1 | export * from './logs.modal'; 2 | export * from './logs.button'; 3 | -------------------------------------------------------------------------------- /src/app/pages/tabs-container/collection-types/grpc/request/index.ts: -------------------------------------------------------------------------------- 1 | export * from './request'; 2 | -------------------------------------------------------------------------------- /src/main/dialog/constants.ts: -------------------------------------------------------------------------------- 1 | export enum DialogChannel { 2 | OPEN = 'dialog:open', 3 | } 4 | -------------------------------------------------------------------------------- /src/main/electron-store/index.ts: -------------------------------------------------------------------------------- 1 | export * from './preload'; 2 | export * from './subscribers'; 3 | -------------------------------------------------------------------------------- /src/app/pages/tabs-container/collection-types/grpc/index.ts: -------------------------------------------------------------------------------- 1 | export * from './grpc-tab-container'; 2 | -------------------------------------------------------------------------------- /src/core/clients/grpc/grpc-web-client/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './metadata.interface'; 2 | -------------------------------------------------------------------------------- /src/core/protobuf/index.ts: -------------------------------------------------------------------------------- 1 | export * from './protobuf-loader'; 2 | export * from './interfaces'; 3 | -------------------------------------------------------------------------------- /src/main/clients/grpc-client/index.ts: -------------------------------------------------------------------------------- 1 | export * from './preload'; 2 | export * from './subscribers'; 3 | -------------------------------------------------------------------------------- /src/main/clients/index.ts: -------------------------------------------------------------------------------- 1 | export * from './grpc-client'; 2 | export * from './grpc-web-client'; 3 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint && npm run test 5 | -------------------------------------------------------------------------------- /src/app/components/select/colored/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './colored-select-option.interface'; 2 | -------------------------------------------------------------------------------- /src/app/components/tabs/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-refs'; 2 | export * from './use-on-screen'; 3 | -------------------------------------------------------------------------------- /src/app/pages/tabs-container/collection-types/grpc/tls/index.ts: -------------------------------------------------------------------------------- 1 | export * from './tls-settings.modal'; 2 | -------------------------------------------------------------------------------- /src/main/clients/grpc-web-client/index.ts: -------------------------------------------------------------------------------- 1 | export * from './preload'; 2 | export * from './subscribers'; 3 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit "${1}" 5 | -------------------------------------------------------------------------------- /src/app/components/color-picker/index.ts: -------------------------------------------------------------------------------- 1 | export * from './color-picker'; 2 | export * from './color-picker.input'; 3 | -------------------------------------------------------------------------------- /src/app/components/notification/index.ts: -------------------------------------------------------------------------------- 1 | export * from './notification-container'; 2 | export * from './hooks'; 3 | -------------------------------------------------------------------------------- /src/app/components/select/colored/index.ts: -------------------------------------------------------------------------------- 1 | export * from './colored-select'; 2 | export * from './interfaces'; 3 | -------------------------------------------------------------------------------- /src/app/pages/collections/forms/fields/include-directories/index.ts: -------------------------------------------------------------------------------- 1 | export * from './include-directories.field'; 2 | -------------------------------------------------------------------------------- /src/app/themes/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dark'; 2 | export * from './light'; 3 | export * from './global.css'; 4 | -------------------------------------------------------------------------------- /src/core/clients/grpc/grpc-client/index.ts: -------------------------------------------------------------------------------- 1 | export * from './grpc-client'; 2 | export * from './metadata-parser'; 3 | -------------------------------------------------------------------------------- /src/app/pages/tabs-container/collection-types/grpc/environments/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-environment.modal'; 2 | -------------------------------------------------------------------------------- /src/app/storage/interfaces/tabs/index.ts: -------------------------------------------------------------------------------- 1 | export * from './grpc-tab.interface'; 2 | export * from './tabs.interface'; 3 | -------------------------------------------------------------------------------- /src/main/protobuf/constants.ts: -------------------------------------------------------------------------------- 1 | export enum ProtobufChannel { 2 | LOAD_FROM_FILE = 'protobuf:load-from-file', 3 | } 4 | -------------------------------------------------------------------------------- /src/app/components/tabs/tab-bar/index.ts: -------------------------------------------------------------------------------- 1 | export * from './tab-bar'; 2 | export type { ActiveBarProps } from './active-bar'; 3 | -------------------------------------------------------------------------------- /src/app/hooks/collections/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-create-collection'; 2 | export * from './use-update-collection'; 3 | -------------------------------------------------------------------------------- /src/app/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './collections'; 2 | export * from './protocols'; 3 | export * from './use-shortcuts'; 4 | -------------------------------------------------------------------------------- /src/app/components/tabs/tab-bar/tab-bar-item/index.ts: -------------------------------------------------------------------------------- 1 | export * from './tab-bar-item'; 2 | export * from './tab-bar-item-draggable'; 3 | -------------------------------------------------------------------------------- /src/app/pages/collections/modals/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-collection.modal'; 2 | export * from './update-collection.modal'; 3 | -------------------------------------------------------------------------------- /src/core/clients/grpc/grpc-web-client/interfaces/metadata.interface.ts: -------------------------------------------------------------------------------- 1 | export declare type GrpcWebMetadataValue = string | string[]; 2 | -------------------------------------------------------------------------------- /src/core/clients/grpc/index.ts: -------------------------------------------------------------------------------- 1 | export * from './grpc-client'; 2 | export * from './grpc-web-client'; 3 | export * from './interfaces'; 4 | -------------------------------------------------------------------------------- /src/app/pages/tabs-container/collection-types/grpc/response/index.ts: -------------------------------------------------------------------------------- 1 | export * from './unary-call.response'; 2 | export * from './streams'; 3 | -------------------------------------------------------------------------------- /src/app/pages/side-bar/nodes/index.ts: -------------------------------------------------------------------------------- 1 | export * from './collection.node'; 2 | export * from './method.node'; 3 | export * from './service.node'; 4 | -------------------------------------------------------------------------------- /scripts/gen-icons.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # npm install -g electron-icon-builder 4 | 5 | electron-icon-builder --input=$1 --output=./appicons 6 | -------------------------------------------------------------------------------- /src/app/components/icons/index.ts: -------------------------------------------------------------------------------- 1 | export * from './horizontal-layout.icon'; 2 | export * from './vertical-layout.icon'; 3 | export * from './ezy.icon'; 4 | -------------------------------------------------------------------------------- /src/core/clients/grpc/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client.interface'; 2 | export * from './response.interface'; 3 | export * from './status.enum'; 4 | -------------------------------------------------------------------------------- /webpack.plugins.js: -------------------------------------------------------------------------------- 1 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); 2 | 3 | module.exports = [new ForkTsCheckerWebpackPlugin()]; 4 | -------------------------------------------------------------------------------- /src/app/pages/shortcuts/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-theme-actions'; 2 | export * from './use-grpc-method-actions'; 3 | export * from './use-environment-actions'; 4 | -------------------------------------------------------------------------------- /src/core/typings.ts: -------------------------------------------------------------------------------- 1 | export * from './protobuf/interfaces'; 2 | export * from './clients/grpc/interfaces'; 3 | export * from './clients/grpc/grpc-web-client/interfaces'; 4 | -------------------------------------------------------------------------------- /__tests__/simple-service/README.md: -------------------------------------------------------------------------------- 1 | ```bash 2 | $ npm run proto 3 | 4 | $ npm run start 5 | 6 | # For gRPC web running on 8080 port 7 | $ docker-compose up 8 | ``` 9 | -------------------------------------------------------------------------------- /.storybook/manager.js: -------------------------------------------------------------------------------- 1 | import { addons } from '@storybook/addons'; 2 | import { themes } from '@storybook/theming'; 3 | 4 | addons.setConfig({ 5 | theme: themes.dark, 6 | }); 7 | -------------------------------------------------------------------------------- /src/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/electron-store/constants.ts: -------------------------------------------------------------------------------- 1 | export enum ElectronStoreChannel { 2 | GET = 'electron-store:get', 3 | SET = 'electron-store:set', 4 | REMOVE = 'electron-store:remove', 5 | } 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .eslintrc.js 2 | commitlint.config.js 3 | webpack.main.config.js 4 | webpack.plugins.js 5 | webpack.renderer.config.js 6 | webpack.rules.js 7 | wallaby.js 8 | node_modules/**/* 9 | -------------------------------------------------------------------------------- /src/app/pages/collections/badge-types/grpc/index.ts: -------------------------------------------------------------------------------- 1 | export * from './proto.badge'; 2 | export * from './service.badge'; 3 | export * from './stream.badge'; 4 | export * from './unary.badge'; 5 | -------------------------------------------------------------------------------- /src/core/clients/grpc/grpc-web-client/index.ts: -------------------------------------------------------------------------------- 1 | export * from './grpc-web-client'; 2 | export * from './grpc-web-call.stream'; 3 | export * from './grpc-web.error'; 4 | export * from './interfaces'; 5 | -------------------------------------------------------------------------------- /src/app/components/select/colored/interfaces/colored-select-option.interface.ts: -------------------------------------------------------------------------------- 1 | export type ColoredSelectOption = { 2 | id: string; 3 | label: string; 4 | url: string; 5 | color: string; 6 | }; 7 | -------------------------------------------------------------------------------- /__tests__/simple-service/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | envoy: 3 | image: envoyproxy/envoy:v1.22.0 4 | ports: 5 | - 8080:8080 6 | volumes: 7 | - ./envoy.yaml:/etc/envoy/envoy.yaml:ro 8 | -------------------------------------------------------------------------------- /src/main/clients/grpc-web-client/preload/index.ts: -------------------------------------------------------------------------------- 1 | import serverStreaming from './server-streaming'; 2 | import unary from './unary'; 3 | 4 | export const GrpcWebClient = { 5 | unary, 6 | serverStreaming, 7 | }; 8 | -------------------------------------------------------------------------------- /src/core/clients/grpc/interfaces/response.interface.ts: -------------------------------------------------------------------------------- 1 | export interface GrpcResponse = Record> { 2 | code: number; 3 | 4 | timestamp: number; 5 | 6 | value: T; 7 | } 8 | -------------------------------------------------------------------------------- /__tests__/basic-service/proto/basic.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message BasicMessage { 4 | string id = 1; 5 | } 6 | 7 | service BasicService { 8 | rpc BasicRequest(BasicMessage) returns (BasicMessage); 9 | } 10 | -------------------------------------------------------------------------------- /src/app/pages/tabs-container/collection-types/grpc/response/streams/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bidirectional-streaming.response'; 2 | export * from './client-streaming.response'; 3 | export * from './server-streaming.response'; 4 | -------------------------------------------------------------------------------- /src/core/__tests__/fixtures/proto/basic.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message BasicMessage { 4 | string id = 1; 5 | } 6 | 7 | service BasicService { 8 | rpc BasicRequest(BasicMessage) returns (BasicMessage); 9 | } 10 | -------------------------------------------------------------------------------- /src/app/pages/collections/badge-types/grpc/proto.badge.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Badge } from '@components'; 4 | 5 | export const ProtoBadge: React.FC = () => ; 6 | -------------------------------------------------------------------------------- /src/app/pages/collections/badge-types/grpc/unary.badge.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Badge } from '@components'; 4 | 5 | export const UnaryBadge: React.FC = () => ; 6 | -------------------------------------------------------------------------------- /src/app/pages/collections/badge-types/grpc/service.badge.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Badge } from '@components'; 4 | 5 | export const ServiceBadge: React.FC = () => ; 6 | -------------------------------------------------------------------------------- /src/main/os/preload.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron'; 2 | 3 | import { OSChannel } from './constants'; 4 | 5 | export const OS = { 6 | get(): Promise { 7 | return ipcRenderer.invoke(OSChannel.GET); 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /src/app/hooks/protocols/grpc/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-server-streaming'; 2 | export * from './use-unary-call'; 3 | export * from './use-client-streaming'; 4 | export * from './use-bidirectional-streaming'; 5 | export * from './use-grpc-tab-context'; 6 | -------------------------------------------------------------------------------- /__tests__/tls-service/proto/tls_service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package tls_service.v1; 4 | 5 | message SimpleMessage { 6 | string id = 1; 7 | } 8 | 9 | service TLSService { 10 | rpc Unary(SimpleMessage) returns (SimpleMessage); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/os/subscribers.ts: -------------------------------------------------------------------------------- 1 | import { ipcMain } from 'electron'; 2 | import * as os from 'os'; 3 | 4 | import { OSChannel } from './constants'; 5 | 6 | export const registerOSSubscribers = () => { 7 | ipcMain.handle(OSChannel.GET, () => os.platform()); 8 | }; 9 | -------------------------------------------------------------------------------- /src/app/pages/tabs-container/collection-types/grpc/tls/system.badge.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Badge } from '@components'; 4 | 5 | export const SystemBadge: React.FC = () => ( 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /src/app/pages/tabs-container/collection-types/grpc/send-header/index.ts: -------------------------------------------------------------------------------- 1 | export * from './send-header.unary-call'; 2 | export * from './send-header.server-streaming'; 3 | export * from './send-header.client-streaming'; 4 | export * from './send-header.bidirectional-streaming'; 5 | -------------------------------------------------------------------------------- /src/app/storage/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './settings.interface'; 2 | export * from './collections.interface'; 3 | export * from './environments.interface'; 4 | export * from './logs.interface'; 5 | export * from './tls-presets.interface'; 6 | export * from './tabs'; 7 | -------------------------------------------------------------------------------- /src/app/renderer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as ReactDOM from 'react-dom/client'; 3 | 4 | import App from './app'; 5 | 6 | const rootElement = document.getElementById('root'); 7 | 8 | if (rootElement) { 9 | ReactDOM.createRoot(rootElement).render(); 10 | } 11 | -------------------------------------------------------------------------------- /src/app/storage/interfaces/logs.interface.ts: -------------------------------------------------------------------------------- 1 | export interface LogItem { 2 | message: string; 3 | } 4 | 5 | export interface LogStorage { 6 | logs: LogItem[]; 7 | newLogsAvailable: boolean; 8 | 9 | createLog: (log: LogItem) => void; 10 | clearLogs: () => void; 11 | markAsReadLogs: () => void; 12 | } 13 | -------------------------------------------------------------------------------- /wallaby.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | return { 3 | autoDetect: true, 4 | 5 | files: [ 6 | 'src/**/*.ts', 7 | '!src/**/*.spec.ts', 8 | ], 9 | 10 | tests: [ 11 | 'src/**/*.spec.ts' 12 | ], 13 | 14 | env: { 15 | type: 'node', 16 | }, 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /src/app/pages/side-bar/nodes/node.styled.ts: -------------------------------------------------------------------------------- 1 | import { styled } from '@nextui-org/react'; 2 | 3 | export const StyledNodeWrapper = styled('div', { 4 | display: 'flex', 5 | flexWrap: 'nowrap', 6 | alignItems: 'center', 7 | paddingLeft: 10, 8 | marginRight: 10, 9 | overflow: 'hidden', 10 | flex: 1, 11 | }); 12 | -------------------------------------------------------------------------------- /src/main/common/parse-error.ts: -------------------------------------------------------------------------------- 1 | export function parseErrorFromIPCMain(error: any): string { 2 | if (error?.message && typeof error.message === 'string') { 3 | const message = error.message.split('Error: '); 4 | 5 | return message.length > 1 ? message[1] : error.message; 6 | } 7 | 8 | return error; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/pages/side-bar/collections-tree.styled.ts: -------------------------------------------------------------------------------- 1 | import { styled } from '@nextui-org/react'; 2 | 3 | export const StyledCollectionsTree = styled('div', { 4 | display: 'flex', 5 | flexWrap: 'nowrap', 6 | flexDirection: 'column', 7 | flex: 1, 8 | 9 | overflow: 'hidden', 10 | 11 | userSelect: 'none', 12 | }); 13 | -------------------------------------------------------------------------------- /src/main/dialog/preload.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer, OpenDialogOptions } from 'electron'; 2 | 3 | import { DialogChannel } from './constants'; 4 | 5 | export const ElectronDialog = { 6 | open(options: OpenDialogOptions): Promise { 7 | return ipcRenderer.invoke(DialogChannel.OPEN, options); 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: 🤔 Long question or ideas? 4 | url: https://github.com/getezy/ezy/discussions 5 | about: Ask long-form questions and discuss ideas. 6 | - name: 📖 Have you read Wiki? 7 | url: https://github.com/getezy/ezy/wiki 8 | about: Open wiki page. 9 | -------------------------------------------------------------------------------- /src/app/themes/light.ts: -------------------------------------------------------------------------------- 1 | import { createTheme } from '@nextui-org/react'; 2 | 3 | export const LightTheme = createTheme({ 4 | type: 'light', 5 | theme: { 6 | colors: { 7 | activeLine: 'rgba(232, 232, 232, 0.5)', 8 | ezy: '#acc917', 9 | gradient: 'linear-gradient(112deg, #acc917 -25%, #4f680f 90%)', 10 | }, 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "@release-it/conventional-changelog": { 4 | "infile": "CHANGELOG.md", 5 | "preset": "conventionalcommits", 6 | "ignoreRecommendedBump": false 7 | } 8 | }, 9 | "git": { 10 | "commitMessage": "chore: release v${version}" 11 | }, 12 | "npm": { 13 | "publish": false 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app/storage/index.ts: -------------------------------------------------------------------------------- 1 | import { enableMapSet } from 'immer'; 2 | 3 | enableMapSet(); 4 | 5 | export * from './interfaces'; 6 | export * from './settings.storage'; 7 | export * from './collections.storage'; 8 | export * from './tabs.storage'; 9 | export * from './environments.storage'; 10 | export * from './logs.storage'; 11 | export * from './tls-presets.storage'; 12 | -------------------------------------------------------------------------------- /src/app/storage/interfaces/environments.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Environment { 2 | id: string; 3 | label: string; 4 | url: string; 5 | color: string; 6 | } 7 | 8 | export interface EnvironmentsStorage { 9 | environments: Environment[]; 10 | 11 | createEnvironment: (environment: Environment) => void; 12 | removeEnvironment: (id: string) => void; 13 | } 14 | -------------------------------------------------------------------------------- /static/entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-jit 6 | 7 | com.apple.security.cs.debugger 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/clients/grpc-client/preload/index.ts: -------------------------------------------------------------------------------- 1 | import bidirectionalStreaming from './bidirectional-streaming'; 2 | import clientStreaming from './client-streaming'; 3 | import serverStreaming from './server-streaming'; 4 | import unary from './unary'; 5 | 6 | export const GrpcClient = { 7 | unary, 8 | serverStreaming, 9 | clientStreaming, 10 | bidirectionalStreaming, 11 | }; 12 | -------------------------------------------------------------------------------- /src/core/clients/grpc/grpc-client/metadata-parser.ts: -------------------------------------------------------------------------------- 1 | import { Metadata, MetadataValue } from '@grpc/grpc-js'; 2 | 3 | export class MetadataParser { 4 | static parse(value: Record): Metadata { 5 | return Object.keys(value).reduce((metadata, key) => { 6 | metadata.set(key, value[key]); 7 | 8 | return metadata; 9 | }, new Metadata()); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/core/clients/grpc/grpc-client/__tests__/metadata-parser.spec.ts: -------------------------------------------------------------------------------- 1 | import { MetadataParser } from '../metadata-parser'; 2 | 3 | describe('MetadataParser', () => { 4 | it('should parse metadata when value defined', () => { 5 | const value = { test: '123' }; 6 | 7 | const metadata = MetadataParser.parse(value); 8 | 9 | expect(metadata.get('test')).toEqual(['123']); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "stories": [ 3 | "../stories/**/*.stories.mdx", 4 | "../stories/**/*.stories.@(js|jsx|ts|tsx)" 5 | ], 6 | "addons": [ 7 | "@storybook/addon-links", 8 | "@storybook/addon-essentials", 9 | "@storybook/addon-interactions" 10 | ], 11 | "framework": "@storybook/react", 12 | "core": { 13 | "builder": "@storybook/builder-webpack5" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /__tests__/basic-service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-service", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "start": "node index.js", 7 | "proto": "grpc_tools_node_protoc --js_out=import_style=commonjs,binary:./ --grpc_out=grpc_js:./ proto/basic.proto" 8 | }, 9 | "dependencies": { 10 | "@grpc/grpc-js": "1.6.8", 11 | "google-protobuf": "3.21.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/themes/dark.ts: -------------------------------------------------------------------------------- 1 | import { createTheme } from '@nextui-org/react'; 2 | 3 | export const DarkTheme = createTheme({ 4 | type: 'dark', 5 | theme: { 6 | colors: { 7 | background: '#202225', 8 | backgroundContrast: '#292b2f', 9 | selection: 'rgb(75, 75, 75)', 10 | activeLine: 'rgba(22, 24, 26, 0.5)', 11 | ezy: '#acc917', 12 | gradient: 'linear-gradient(112deg, #acc917 -25%, #4f680f 90%)', 13 | }, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /src/core/clients/grpc/grpc-web-client/metadata-parser.ts: -------------------------------------------------------------------------------- 1 | import { grpc } from '@improbable-eng/grpc-web'; 2 | 3 | import { GrpcWebMetadataValue } from './interfaces'; 4 | 5 | export class MetadataParser { 6 | static parse(value: Record): grpc.Metadata { 7 | return Object.keys(value).reduce((metadata, key) => { 8 | metadata.set(key, value[key]); 9 | 10 | return metadata; 11 | }, new grpc.Metadata()); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/protobuf/subscribers.ts: -------------------------------------------------------------------------------- 1 | import { ipcMain } from 'electron'; 2 | 3 | import { GrpcOptions, ProtobufLoader } from '@core'; 4 | 5 | import { ProtobufChannel } from './constants'; 6 | 7 | export const registerProtobufSubscribers = () => { 8 | ipcMain.handle(ProtobufChannel.LOAD_FROM_FILE, async (_event, options: GrpcOptions) => { 9 | const ast = await ProtobufLoader.loadFromFile(options); 10 | 11 | return ProtobufLoader.parse(ast); 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /src/app/pages/tabs-container/collection-types/grpc/response/streams/response.styled.ts: -------------------------------------------------------------------------------- 1 | import { styled } from '@nextui-org/react'; 2 | 3 | export const StyledContainer = styled('div', { 4 | display: 'flex', 5 | flex: 1, 6 | 7 | overflow: 'hidden', 8 | }); 9 | 10 | export const ListWrapper = styled('ul', { 11 | display: 'flex', 12 | flexDirection: 'column', 13 | flex: 1, 14 | 15 | margin: 0, 16 | 17 | overflow: 'auto', 18 | 19 | backgroundColor: '$background', 20 | }); 21 | -------------------------------------------------------------------------------- /src/core/clients/grpc/interfaces/status.enum.ts: -------------------------------------------------------------------------------- 1 | export enum GrpcStatus { 2 | OK = 0, 3 | CANCELED = 1, 4 | UNKNOWN = 2, 5 | INVALID_ARGUMENT = 3, 6 | DEADLINE_EXCEEDED = 4, 7 | NOT_FOUND = 5, 8 | ALREADY_EXISTS = 6, 9 | PERMISSION_DENIED = 7, 10 | RESOURCE_EXHAUSTED = 8, 11 | FAILED_PRECONDITION = 9, 12 | ABORTED = 10, 13 | OUT_OF_RANGE = 11, 14 | UNIMPLEMENTED = 12, 15 | INTERNAL = 13, 16 | UNAVAILABLE = 14, 17 | DATA_LOSS = 15, 18 | UNAUTHENTICATED = 16, 19 | } 20 | -------------------------------------------------------------------------------- /src/core/clients/grpc/grpc-web-client/grpc-web.error.ts: -------------------------------------------------------------------------------- 1 | import { grpc } from '@improbable-eng/grpc-web'; 2 | 3 | export class GrpcWebError extends Error { 4 | constructor( 5 | readonly code: grpc.Code, 6 | readonly details: string, 7 | readonly metadata?: grpc.Metadata 8 | ) { 9 | super(details); 10 | } 11 | 12 | toObject() { 13 | return { 14 | code: this.code, 15 | details: this.details, 16 | metadata: this.metadata?.headersMap, 17 | }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.workingDirectories": [ 3 | { 4 | "directory": ".", 5 | "changeProcessCWD": true 6 | }, 7 | { 8 | "directory": "__tests__/simple-service", 9 | "changeProcessCWD": true 10 | }, 11 | { 12 | "directory": "__tests__/tls-service", 13 | "changeProcessCWD": true 14 | }, 15 | ], 16 | "yaml.schemas": { 17 | "https://json.schemastore.org/github-issue-config.json": "./.github/ISSUE_TEMPLATE/config.yml" 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /__tests__/simple-service/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "module": "CommonJS", 5 | "target": "ESNext", 6 | "lib": [ 7 | "esnext" 8 | ], 9 | "strict": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "noImplicitAny": true, 13 | "sourceMap": true, 14 | "baseUrl": "./src", 15 | "outDir": "dist", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | }, 19 | "include": ["src/**/*"], 20 | } 21 | -------------------------------------------------------------------------------- /__tests__/tls-service/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "module": "CommonJS", 5 | "target": "ESNext", 6 | "lib": [ 7 | "esnext" 8 | ], 9 | "strict": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "noImplicitAny": true, 13 | "sourceMap": true, 14 | "baseUrl": "./src", 15 | "outDir": "dist", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | }, 19 | "include": ["src/**/*"], 20 | } 21 | -------------------------------------------------------------------------------- /src/app/components/tabs/tab-bar/tab-bar-item/tab-bar-item.tsx: -------------------------------------------------------------------------------- 1 | import React, { PropsWithChildren } from 'react'; 2 | 3 | import { StyledTabBarItem, TabBarItemProps } from './tab-bar-item.styled'; 4 | 5 | export const TabBarItem = React.forwardRef>( 6 | ({ children, active = false, closable = false, onClick }, ref) => ( 7 | 8 | {children} 9 | 10 | ) 11 | ); 12 | -------------------------------------------------------------------------------- /src/app/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './color-circle'; 2 | export * from './color-picker'; 3 | export * from './select'; 4 | export * from './resizable-panel'; 5 | export * from './icons'; 6 | export * from './badge'; 7 | export * from './code-editor'; 8 | export * from './tabs'; 9 | export * from './tree'; 10 | export * from './file-input'; 11 | export * from './notification'; 12 | export * from './info-label'; 13 | export * from './kbar'; 14 | export * from './menu'; 15 | export * from './buttons'; 16 | export * from './kbd'; 17 | -------------------------------------------------------------------------------- /src/app/components/buttons/ezy.button.tsx: -------------------------------------------------------------------------------- 1 | import { Button, ButtonProps } from '@nextui-org/react'; 2 | import React from 'react'; 3 | 4 | export const EzyButton: React.FC = ({ css, children, ...props }) => ( 5 | 20 | ); 21 | -------------------------------------------------------------------------------- /src/app/storage/interfaces/tls-presets.interface.ts: -------------------------------------------------------------------------------- 1 | import { GrpcTlsConfig, GrpcTlsType } from '@core/types'; 2 | 3 | export interface TlsPreset { 4 | id: string; 5 | name: string; 6 | system: boolean; 7 | tls: GrpcTlsConfig; 8 | } 9 | 10 | export interface TlsPresetsStorage { 11 | presets: TlsPreset[]; 12 | 13 | createTlsPreset: (preset: Omit) => void; 14 | updateTlsPreset: (id: string, preset: Omit) => void; 15 | removeTlsPreset: (id: string) => void; 16 | } 17 | -------------------------------------------------------------------------------- /src/core/protobuf/interfaces/protobuf-loader.interface.ts: -------------------------------------------------------------------------------- 1 | export enum GrpcMethodType { 2 | UNARY = 'unary', 3 | SERVER_STREAMING = 'server-streaming', 4 | CLIENT_STREAMING = 'client-streaming', 5 | BIDIRECTIONAL_STREAMING = 'bidirectional-streaming', 6 | } 7 | 8 | export type GrpcMethodInfo = { 9 | name: string; 10 | type: GrpcMethodType; 11 | }; 12 | 13 | export type GrpcServiceInfo = { 14 | name: string; 15 | methods?: GrpcMethodInfo[]; 16 | }; 17 | 18 | export type GrpcOptions = { 19 | path: string; 20 | includeDirs?: string[]; 21 | }; 22 | -------------------------------------------------------------------------------- /src/main/protobuf/preload.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron'; 2 | 3 | import { GrpcOptions, GrpcServiceInfo } from '@core'; 4 | 5 | import { parseErrorFromIPCMain } from '../common'; 6 | import { ProtobufChannel } from './constants'; 7 | 8 | export const Protobuf = { 9 | async loadFromFile(options: GrpcOptions): Promise { 10 | try { 11 | const ast = await ipcRenderer.invoke(ProtobufChannel.LOAD_FROM_FILE, options); 12 | 13 | return ast; 14 | } catch (error) { 15 | throw new Error(parseErrorFromIPCMain(error)); 16 | } 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /src/app/components/tabs/tab.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-unused-prop-types */ 2 | 3 | import { styled, VariantProps } from '@nextui-org/react'; 4 | import React, { PropsWithChildren } from 'react'; 5 | 6 | const StyledTab = styled('div', { 7 | display: 'flex', 8 | flex: 1, 9 | overflow: 'hidden', 10 | }); 11 | 12 | export type TabProps = { 13 | title: string; 14 | 15 | id: string; 16 | 17 | closable?: boolean; 18 | } & VariantProps; 19 | 20 | export const Tab: React.FC> = ({ children }) => ( 21 | {children} 22 | ); 23 | -------------------------------------------------------------------------------- /src/main/clients/grpc-web-client/preload/handlers.ts: -------------------------------------------------------------------------------- 1 | import { IpcRendererEvent } from 'electron'; 2 | 3 | import { GrpcWebError } from '@core'; 4 | 5 | export type OnDataCallback = (data: Record) => void; 6 | 7 | export type OnErrorCallback = (error: GrpcWebError) => void; 8 | 9 | export type OnEndCallback = () => void; 10 | 11 | export function wrapHandler(streamId: string, callback: (...callbackArgs: any[]) => void) { 12 | return function wrappedHandler(event: IpcRendererEvent, id: string, ...args: any[]) { 13 | if (streamId === id) { 14 | callback(...args); 15 | } 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /__tests__/simple-service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-service", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "start": "ts-node ./src/index.ts", 7 | "proto": "protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_opt=env=node,outputClientImpl=false,outputServices=grpc-js,esModuleInterop=true,forceLong=long --ts_proto_out=./src/generated ./proto/simple.proto" 8 | }, 9 | "dependencies": { 10 | "@grpc/grpc-js": "1.6.8" 11 | }, 12 | "devDependencies": { 13 | "ts-node": "10.9.1", 14 | "ts-proto": "1.121.1", 15 | "typescript": "4.7.4" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/components/tabs/tab-bar/tab-bar.styled.ts: -------------------------------------------------------------------------------- 1 | import { styled } from '@nextui-org/react'; 2 | 3 | export const TabBarWrapper = styled('div', { 4 | display: 'flex', 5 | flexWrap: 'nowrap', 6 | overflow: 'hidden', 7 | }); 8 | 9 | export const TabBarRightNodeWrapper = styled('div', { 10 | display: 'flex', 11 | flexWrap: 'nowrap', 12 | alignItems: 'center', 13 | marginLeft: 'auto', 14 | }); 15 | 16 | export const StyledTabBar = styled('div', { 17 | position: 'relative', 18 | display: 'flex', 19 | flex: 1, 20 | flexWrap: 'nowrap', 21 | overflow: 'auto', 22 | 23 | background: '$background', 24 | }); 25 | -------------------------------------------------------------------------------- /src/core/__tests__/fixtures/proto/simple.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package simple_package.v1; 4 | 5 | import "google/protobuf/empty.proto"; 6 | 7 | message SimpleMessage { 8 | string id = 1; 9 | } 10 | 11 | service SimpleService { 12 | rpc SimpleUnaryRequest(SimpleMessage) returns (google.protobuf.Empty); 13 | 14 | rpc SimpleClientStreamRequest(stream SimpleMessage) returns (SimpleMessage); 15 | 16 | rpc SimpleServerStreamRequest(google.protobuf.Empty) 17 | returns (stream SimpleMessage); 18 | 19 | rpc SimpleBidirectionalStreamRequest(stream SimpleMessage) 20 | returns (stream SimpleMessage); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/clients/grpc-client/preload/handlers.ts: -------------------------------------------------------------------------------- 1 | import { ServerErrorResponse } from '@grpc/grpc-js'; 2 | import { IpcRendererEvent } from 'electron'; 3 | 4 | export type OnDataCallback = (data: Record) => void; 5 | 6 | export type OnErrorCallback = (error: ServerErrorResponse) => void; 7 | 8 | export type OnEndCallback = () => void; 9 | 10 | export function wrapHandler(streamId: string, callback: (...callbackArgs: any[]) => void) { 11 | return function wrappedHandler(event: IpcRendererEvent, id: string, ...args: any[]) { 12 | if (streamId === id) { 13 | callback(...args); 14 | } 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/electron-store/subscribers.ts: -------------------------------------------------------------------------------- 1 | import { ipcMain } from 'electron'; 2 | import ElectronStore from 'electron-store'; 3 | 4 | import { ElectronStoreChannel } from './constants'; 5 | 6 | const store = new ElectronStore(); 7 | 8 | export const registerElectronStoreSubscribers = () => { 9 | ipcMain.handle(ElectronStoreChannel.GET, async (_event, value) => store.get(value)); 10 | 11 | ipcMain.handle(ElectronStoreChannel.SET, async (_event, key, value) => { 12 | store.set(key, value); 13 | }); 14 | 15 | ipcMain.handle(ElectronStoreChannel.REMOVE, async (_event, key) => { 16 | store.reset(key); 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /src/main/dialog/subscribers.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, dialog, ipcMain, OpenDialogOptions } from 'electron'; 2 | 3 | import { DialogChannel } from './constants'; 4 | 5 | export const registerDialogSubscribers = (mainWindow: BrowserWindow) => { 6 | ipcMain.handle(DialogChannel.OPEN, async (_event, options: OpenDialogOptions) => { 7 | const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, options); 8 | 9 | if (!canceled) { 10 | return filePaths; 11 | } 12 | 13 | return []; 14 | }); 15 | }; 16 | 17 | export const unregisterDialogSubscribers = () => { 18 | ipcMain.removeHandler(DialogChannel.OPEN); 19 | }; 20 | -------------------------------------------------------------------------------- /webpack.main.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | /** 5 | * This is the main entry point for your application, it's the first file 6 | * that runs in the main process. 7 | */ 8 | entry: './src/main/index.ts', 9 | // Put your normal webpack config below here 10 | module: { 11 | rules: require('./webpack.rules'), 12 | }, 13 | resolve: { 14 | extensions: ['.js', '.ts', '.jsx', '.tsx', '.css', '.json'], 15 | alias: { 16 | '@core$': path.resolve(__dirname, './src/core/index.ts'), 17 | '@core/types': path.resolve(__dirname, './src/core/typings.ts'), 18 | }, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /src/app/components/tabs/hooks/use-refs.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export function useRefs(): [ 4 | (key: React.Key) => React.RefObject, 5 | (key: React.Key) => void 6 | ] { 7 | const cacheRefs = React.useRef(new Map>()); 8 | 9 | function getRef(key: React.Key) { 10 | if (!cacheRefs.current.has(key)) { 11 | cacheRefs.current.set(key, React.createRef()); 12 | } 13 | 14 | return cacheRefs.current.get(key)!; 15 | } 16 | 17 | function removeRef(key: React.Key) { 18 | cacheRefs.current.delete(key); 19 | } 20 | 21 | return [getRef, removeRef]; 22 | } 23 | -------------------------------------------------------------------------------- /__tests__/tls-service/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | envoy-server: 3 | image: envoyproxy/envoy:v1.22.0 4 | ports: 5 | - 8080:8080 6 | volumes: 7 | - ./envoy-server.yaml:/etc/envoy/envoy.yaml:ro 8 | - ./certs/server-cert.pem:/etc/server-cert.pem 9 | - ./certs/server-key.pem:/etc/server-key.pem 10 | envoy-mutual: 11 | image: envoyproxy/envoy:v1.22.0 12 | ports: 13 | - 8080:8080 14 | volumes: 15 | - ./envoy-mutual.yaml:/etc/envoy/envoy.yaml:ro 16 | - ./certs/ca-cert.pem:/etc/ca-cert.pem 17 | - ./certs/server-cert.pem:/etc/server-cert.pem 18 | - ./certs/server-key.pem:/etc/server-key.pem 19 | -------------------------------------------------------------------------------- /src/app/storage/interfaces/settings.interface.ts: -------------------------------------------------------------------------------- 1 | export enum Language { 2 | EN = 'en', 3 | } 4 | 5 | export enum ThemeType { 6 | DARK = 'dark', 7 | LIGHT = 'light', 8 | } 9 | 10 | export enum Alignment { 11 | VERTICAL = 'vertical', 12 | HORIZONTAL = 'horizontal', 13 | } 14 | 15 | export interface Settings { 16 | theme: ThemeType; 17 | language: Language; 18 | alignment: Alignment; 19 | isMenuCollapsed: boolean; 20 | } 21 | 22 | export interface SettingsStorage extends Settings { 23 | updateTheme: (theme: ThemeType) => void; 24 | updateAlignment: (alignment: Alignment) => void; 25 | setIsMenuCollapsed: (isCollapsed: boolean) => void; 26 | } 27 | -------------------------------------------------------------------------------- /src/app/components/tabs/tab-content.tsx: -------------------------------------------------------------------------------- 1 | import { styled, VariantProps } from '@nextui-org/react'; 2 | import React, { PropsWithChildren } from 'react'; 3 | 4 | const StyledTabContent = styled('div', { 5 | display: 'flex', 6 | flex: 1, 7 | overflow: 'hidden', 8 | 9 | variants: { 10 | active: { 11 | false: { 12 | display: 'none', 13 | }, 14 | }, 15 | }, 16 | }); 17 | 18 | export type TabContentProps = {} & VariantProps; 19 | 20 | export const TabContent: React.FC> = ({ 21 | children, 22 | active = false, 23 | }) => {children}; 24 | -------------------------------------------------------------------------------- /src/main/preload.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge } from 'electron'; 2 | 3 | import { GrpcClient, GrpcWebClient } from './clients'; 4 | import { ElectronDialog } from './dialog'; 5 | import { ElectronStore } from './electron-store'; 6 | import { OS } from './os'; 7 | import { Protobuf } from './protobuf'; 8 | 9 | contextBridge.exposeInMainWorld('electronDialog', ElectronDialog); 10 | 11 | contextBridge.exposeInMainWorld('electronStore', ElectronStore); 12 | 13 | contextBridge.exposeInMainWorld('protobuf', Protobuf); 14 | 15 | contextBridge.exposeInMainWorld('clients', { 16 | grpc: GrpcClient, 17 | grpcWeb: GrpcWebClient, 18 | }); 19 | 20 | contextBridge.exposeInMainWorld('os', OS); 21 | -------------------------------------------------------------------------------- /src/app/components/kbd/kbd.ts: -------------------------------------------------------------------------------- 1 | import { styled } from '@nextui-org/react'; 2 | 3 | export const Kbd = styled('kbd', { 4 | padding: '4px 6px', 5 | background: 'rgba(0 0 0 / .1)', 6 | borderRadius: '4px', 7 | fontSize: '$$kbdFontSize', 8 | margin: 0, 9 | 10 | variants: { 11 | size: { 12 | xs: { 13 | $$kbdFontSize: '10px', 14 | }, 15 | sm: { 16 | $$kbdFontSize: '$fontSizes$sm', 17 | }, 18 | md: { 19 | $$kbdFontSize: '$fontSizes$md', 20 | }, 21 | lg: { 22 | $$kbdFontSize: '$fontSizes$lg', 23 | }, 24 | xl: { 25 | $$kbdFontSize: '$fontSizes$xl', 26 | }, 27 | }, 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /src/app/components/notification/hooks/use-notification.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Id, toast, ToastOptions } from 'react-toastify'; 3 | 4 | import { Notification } from '../notification'; 5 | 6 | export type NotificationMessage = { 7 | title: string; 8 | description?: string; 9 | }; 10 | 11 | export function notification(message: NotificationMessage, options?: ToastOptions): Id { 12 | return toast(, options); 13 | } 14 | 15 | export function closeNotification(id: Id): void { 16 | toast.dismiss(id); 17 | } 18 | 19 | export function closeAllNotifications(): void { 20 | toast.dismiss(); 21 | } 22 | -------------------------------------------------------------------------------- /__tests__/basic-service/index.js: -------------------------------------------------------------------------------- 1 | var messages = require('./proto/basic_pb'); 2 | var services = require('./proto/basic_grpc_pb'); 3 | 4 | var grpc = require('@grpc/grpc-js'); 5 | 6 | function basicRequest(call, callback) { 7 | var messagee = new messages.BasicMessage(); 8 | messagee.setId(call.request.getId()); 9 | callback(null, messagee); 10 | } 11 | 12 | function main() { 13 | var server = new grpc.Server(); 14 | server.addService(services.BasicServiceService, { basicRequest }); 15 | server.bindAsync('0.0.0.0:4000', grpc.ServerCredentials.createInsecure(), () => { 16 | server.start(); 17 | console.log('gRPC server started on 0.0.0.0:4000'); 18 | }); 19 | } 20 | 21 | main(); 22 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | import { GrpcClient } from './main/clients/grpc-client/preload'; 2 | import { GrpcWebClient } from './main/clients/grpc-web-client/preload'; 3 | import { ElectronDialog } from './main/dialog/preload'; 4 | import { ElectronStore } from './main/electron-store/preload'; 5 | import { OS } from './main/os/preload'; 6 | import { Protobuf } from './main/protobuf/preload'; 7 | 8 | declare global { 9 | interface Window { 10 | electronStore: typeof ElectronStore; 11 | electronDialog: typeof ElectronDialog; 12 | protobuf: typeof Protobuf; 13 | clients: { 14 | grpc: typeof GrpcClient; 15 | grpcWeb: typeof GrpcWebClient; 16 | }; 17 | os: typeof OS; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/themes/global.css.ts: -------------------------------------------------------------------------------- 1 | import { globalCss } from '@nextui-org/react'; 2 | 3 | export const globalStyles = globalCss({ 4 | '#root > div': { 5 | height: '100vh', 6 | width: '100vw', 7 | }, 8 | 9 | // For displaying toasts on top of modals 10 | '.nextui-backdrop': { 11 | zIndex: '9998 !important', 12 | }, 13 | 14 | '::-webkit-scrollbar': { 15 | height: 2, 16 | width: 2, 17 | }, 18 | '::-webkit-scrollbar-track': { 19 | backgroundColor: '$backgroundContrast', 20 | }, 21 | '::-webkit-scrollbar-thumb': { 22 | boxShadow: 'inset 0 0 6px', 23 | color: '$accents5', 24 | }, 25 | '::-webkit-scrollbar-corner': { 26 | backgroundColor: '$backgroundContrast', 27 | }, 28 | }); 29 | -------------------------------------------------------------------------------- /src/main/electron-store/preload.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron'; 2 | 3 | import { ElectronStoreChannel } from './constants'; 4 | 5 | export const ElectronStore = { 6 | getItem(key: string): Promise { 7 | return ipcRenderer.invoke(ElectronStoreChannel.GET, key); 8 | }, 9 | setItem(key: string, value: T): Promise { 10 | setTimeout(() => { 11 | ipcRenderer.invoke(ElectronStoreChannel.SET, key, value); 12 | }, 0); 13 | 14 | return Promise.resolve(); 15 | }, 16 | removeItem(key: string): Promise { 17 | setTimeout(() => { 18 | ipcRenderer.invoke(ElectronStoreChannel.REMOVE, key); 19 | }, 0); 20 | 21 | return Promise.resolve(); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: Test & Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | pull_request: 8 | branches: 9 | - '**' 10 | workflow_call: 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | node-version: [18.x] 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | cache: npm 26 | 27 | - name: Install dependencies 28 | run: npm ci 29 | 30 | - name: lint 31 | run: npm run lint 32 | 33 | - name: test 34 | run: npm run test 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F680 Feature request" 3 | about: Suggest an idea or a feature request. 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /scripts/add-osx-cert.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | KEY_CHAIN=build.keychain 4 | CERTIFICATE_P12=certificate.p12 5 | 6 | # Recreate the certificate from the secure environment variable 7 | echo $CERTIFICATE_OSX_APPLICATION | base64 --decode > $CERTIFICATE_P12 8 | 9 | #create a keychain 10 | security create-keychain -p actions $KEY_CHAIN 11 | 12 | # Make the keychain the default so identities are found 13 | security default-keychain -s $KEY_CHAIN 14 | 15 | # Unlock the keychain 16 | security unlock-keychain -p actions $KEY_CHAIN 17 | 18 | security import $CERTIFICATE_P12 -k $KEY_CHAIN -P $CERTIFICATE_PASSWORD -T /usr/bin/codesign; 19 | 20 | security set-key-partition-list -S apple-tool:,apple: -s -k actions $KEY_CHAIN 21 | 22 | # remove certs 23 | rm -fr *.p12 24 | -------------------------------------------------------------------------------- /webpack.rules.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | // Add support for native node modules 3 | { 4 | // We're specifying native_modules in the test because the asset relocator loader generates a 5 | // "fake" .node file which is really a cjs file. 6 | test: /native_modules\/.+\.node$/, 7 | use: 'node-loader', 8 | }, 9 | { 10 | test: /\.(m?js|node)$/, 11 | parser: { amd: false }, 12 | use: { 13 | loader: '@vercel/webpack-asset-relocator-loader', 14 | options: { 15 | outputAssetBase: 'native_modules', 16 | }, 17 | }, 18 | }, 19 | { 20 | test: /\.tsx?$/, 21 | exclude: /(node_modules|\.webpack)/, 22 | use: { 23 | loader: 'ts-loader', 24 | options: { 25 | transpileOnly: true, 26 | }, 27 | }, 28 | }, 29 | ]; 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41E Bug report" 3 | about: Create a report to help us improve. 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Environment** 27 | - OS: [e.g. windows 11 22H2] 28 | - ezy Version [1.0.0-beta.13.2] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /src/app/app.tsx: -------------------------------------------------------------------------------- 1 | import { NextUIProvider } from '@nextui-org/react'; 2 | import React from 'react'; 3 | 4 | import { NotificationContainer } from '@components'; 5 | import { ThemeType, useSettingsStore } from '@storage'; 6 | 7 | import { Main } from './pages'; 8 | import { DarkTheme, globalStyles, LightTheme } from './themes'; 9 | 10 | export const THEMES = { 11 | [ThemeType.DARK]: DarkTheme, 12 | [ThemeType.LIGHT]: LightTheme, 13 | }; 14 | 15 | function App(): JSX.Element { 16 | const theme = useSettingsStore((store) => store.theme); 17 | 18 | globalStyles(); 19 | 20 | return ( 21 | theme && ( 22 | 23 | 24 |
25 | 26 | ) 27 | ); 28 | } 29 | 30 | export default App; 31 | -------------------------------------------------------------------------------- /src/main/clients/grpc-web-client/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents channel from renderer process 3 | */ 4 | export enum GrpcWebClientChannel { 5 | INVOKE_UNARY_REQUEST = 'grpc-web-client-channel:unary-request:invoke', 6 | 7 | INVOKE_SERVER_STREAMING_REQUEST = 'grpc-web-client-channel:server-streaming-request:invoke', 8 | CANCEL_SERVER_STREAMING_REQUEST = 'grpc-web-client-channel:server-streaming-request:cancel', 9 | } 10 | 11 | /** 12 | * Represents channel from main process 13 | */ 14 | export enum GrpcWebClientServerStreamingChannel { 15 | DATA = 'grpc-web-client:server-streaming-request:data', 16 | ERROR = 'grpc-web-client:server-streaming-request:error', 17 | END = 'grpc-web-client:server-streaming-request:end', 18 | CANCEL = 'grpc-web-client:server-streaming-request:cancel', 19 | } 20 | -------------------------------------------------------------------------------- /src/app/layouts/default.tsx: -------------------------------------------------------------------------------- 1 | import { Container } from '@nextui-org/react'; 2 | import React from 'react'; 3 | 4 | export interface DefaultLayoutProps { 5 | left?: React.ReactNode; 6 | bottom?: React.ReactNode; 7 | } 8 | 9 | export const DefaultLayout: React.FC> = ({ 10 | children, 11 | left, 12 | bottom, 13 | }) => ( 14 | 21 | 28 | {left} 29 | {children} 30 | 31 | {bottom} 32 | 33 | ); 34 | -------------------------------------------------------------------------------- /__tests__/tls-service/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: ['./tsconfig.json'] 5 | }, 6 | plugins: [ 7 | 'simple-import-sort', 8 | 'prettier' 9 | ], 10 | extends: [ 11 | 'airbnb', 12 | 'airbnb/hooks', 13 | 'airbnb-typescript', 14 | 'prettier', 15 | ], 16 | env: { 17 | es6: true, 18 | node: true 19 | }, 20 | rules: { 21 | 'prettier/prettier': ['error', { 22 | printWidth: 100, 23 | singleQuote: true, 24 | trailingComma: 'es5' 25 | }], 26 | 27 | 'import/prefer-default-export': 'off', 28 | 29 | 'simple-import-sort/imports': [ 30 | 2, 31 | { 'groups': [['^\\u0000'], ['^[^.]'], ['^\\.'], ['^.+\\.s?css$']] } 32 | ], 33 | 34 | 'no-plusplus': 'off' 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /__tests__/simple-service/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: ['./tsconfig.json'] 5 | }, 6 | plugins: [ 7 | 'simple-import-sort', 8 | 'prettier' 9 | ], 10 | extends: [ 11 | 'airbnb', 12 | 'airbnb/hooks', 13 | 'airbnb-typescript', 14 | 'prettier', 15 | ], 16 | env: { 17 | es6: true, 18 | node: true 19 | }, 20 | rules: { 21 | 'prettier/prettier': ['error', { 22 | printWidth: 100, 23 | singleQuote: true, 24 | trailingComma: 'es5' 25 | }], 26 | 27 | 'import/prefer-default-export': 'off', 28 | 29 | 'simple-import-sort/imports': [ 30 | 2, 31 | { 'groups': [['^\\u0000'], ['^[^.]'], ['^\\.'], ['^.+\\.s?css$']] } 32 | ], 33 | 34 | 'no-plusplus': 'off' 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /src/main/clients/grpc-web-client/subscribers/index.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, ipcMain } from 'electron'; 2 | 3 | import { GrpcWebClientServerStreamingSubscriber } from './server-streaming.subscriber'; 4 | import { GrpcWebClientUnarySubscriber } from './unary.subscriber'; 5 | 6 | export const registerGrpcWebClientSubscribers = (mainWindow: BrowserWindow) => { 7 | const server = new GrpcWebClientServerStreamingSubscriber(mainWindow, ipcMain); 8 | const unary = new GrpcWebClientUnarySubscriber(mainWindow, ipcMain); 9 | 10 | server.registerServerStreamingHandlers(); 11 | unary.registerUnaryCallHandlers(); 12 | }; 13 | 14 | export const unregisterGrpcWebClientSubscribers = () => { 15 | GrpcWebClientServerStreamingSubscriber.unregisterServerStreamingHandlers(ipcMain); 16 | GrpcWebClientUnarySubscriber.unregisterUnaryCallHandlers(ipcMain); 17 | }; 18 | -------------------------------------------------------------------------------- /__tests__/tls-service/README.md: -------------------------------------------------------------------------------- 1 | ## gRPC 2 | 3 | ### Server 4 | ```bash 5 | # Generate certs and proto definitions 6 | $ npm run build 7 | 8 | $ npm run start:server 9 | 10 | # OR 11 | 12 | $ npm run start:mutual 13 | ``` 14 | ### Client 15 | ```bash 16 | $ npm run client:server 17 | 18 | # OR 19 | 20 | $ npm run client:mutual 21 | ``` 22 | 23 | 24 | ## gRPC-web 25 | 26 | ### Server 27 | If you want to run default (browser-like) server side TLS 28 | 29 | ```bash 30 | $ npm run start:server 31 | 32 | # Server-Side TLS 33 | $ docker-compose up envoy-server 34 | ``` 35 | 36 | For mutual TLS I found the way when you can start grpc service with Server-Side TLS and then run envoy proxy wich verifying client certificate. 37 | 38 | ```bash 39 | $ npm run start:server 40 | 41 | # Mutual TLS 42 | $ docker-compose up envoy-mutual 43 | ``` 44 | -------------------------------------------------------------------------------- /src/app/components/notification/notification-container.tsx: -------------------------------------------------------------------------------- 1 | import { styled, useTheme } from '@nextui-org/react'; 2 | import React from 'react'; 3 | import { ToastContainer } from 'react-toastify'; 4 | 5 | import 'react-toastify/dist/ReactToastify.css'; 6 | 7 | const StyledToastContainer = styled(ToastContainer, { 8 | position: 'fixed', 9 | zIndex: '$max', 10 | '.Toastify__toast-theme--dark': { 11 | backgroundColor: '$backgroundContrast !important', 12 | }, 13 | }); 14 | 15 | export const NotificationContainer: React.FC = () => { 16 | const theme = useTheme(); 17 | 18 | return ( 19 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/app/pages/tabs-container/collection-types/grpc/response/empty-response-view.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Text } from '@nextui-org/react'; 2 | import React from 'react'; 3 | 4 | import { Kbd } from '@components'; 5 | import { ShortcutsGroup, useShortcuts } from '@hooks'; 6 | 7 | export const EmptyResponseView: React.FC = () => { 8 | const { getShortcuts } = useShortcuts(); 9 | 10 | const shortcuts = getShortcuts(ShortcutsGroup.RESPONSE); 11 | 12 | return ( 13 | 14 | {shortcuts.map((shortcut) => ( 15 | <> 16 | 17 | {shortcut.description} 18 | 19 | 20 | {shortcut.key} 21 | 22 | 23 | ))} 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/app/components/tree/collapse.button.tsx: -------------------------------------------------------------------------------- 1 | import { faAngleDown, faAngleLeft } from '@fortawesome/free-solid-svg-icons'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { Button } from '@nextui-org/react'; 4 | import React from 'react'; 5 | 6 | export type CollapseButtonProps = { 7 | isOpen: boolean; 8 | 9 | onClick: (isOpen: boolean) => void; 10 | }; 11 | 12 | export const CollapseButton: React.FC = ({ isOpen, onClick }) => ( 13 | 36 | 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /src/app/storage/interfaces/collections.interface.ts: -------------------------------------------------------------------------------- 1 | import { GrpcMethodInfo, GrpcOptions, GrpcServiceInfo } from '@core/types'; 2 | 3 | export enum CollectionType { 4 | GRPC = 'grpc', 5 | } 6 | 7 | export interface GrpcMethod extends GrpcMethodInfo { 8 | id: string; 9 | } 10 | export interface GrpcService extends GrpcServiceInfo { 11 | id: string; 12 | methods?: GrpcMethod[]; 13 | } 14 | 15 | export type CollectionChildren = T extends CollectionType.GRPC 16 | ? GrpcService[] 17 | : never; 18 | 19 | export type CollectionOptions = T extends CollectionType.GRPC 20 | ? GrpcOptions 21 | : never; 22 | 23 | export interface Collection { 24 | id: string; 25 | name: string; 26 | type: T; 27 | children?: CollectionChildren; 28 | options: CollectionOptions; 29 | } 30 | 31 | export interface CollectionsStorage { 32 | collections: Collection[]; 33 | 34 | createCollection: (collection: Omit, 'id'>) => Promise; 35 | updateCollection: ( 36 | id: string, 37 | collection: Omit, 'id'>, 38 | showSuccessNotification?: boolean 39 | ) => Promise; 40 | removeCollection: (id: string) => void; 41 | filterCollections: (search: string) => Collection[]; 42 | } 43 | -------------------------------------------------------------------------------- /src/app/components/icons/horizontal-layout.icon.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@nextui-org/react'; 2 | import React from 'react'; 3 | 4 | export const HorizontalLayoutIcon: React.FC> = ({ onClick }) => { 5 | const { theme } = useTheme(); 6 | 7 | const [isHovered, setIsHovered] = React.useState(false); 8 | 9 | return ( 10 | setIsHovered(true)} 18 | onMouseLeave={() => setIsHovered(false)} 19 | > 20 | 26 | 32 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /src/app/pages/side-bar/nodes/service.node.tsx: -------------------------------------------------------------------------------- 1 | import { Spacer, Text, Tooltip } from '@nextui-org/react'; 2 | import React from 'react'; 3 | 4 | import { Tree, TreeNode, TreeNodeRendererProps } from '@components'; 5 | import { GrpcMethod, GrpcService } from '@storage'; 6 | 7 | import { ServiceBadge } from '../../collections/badge-types'; 8 | import { GrpcMethodNode } from './method.node'; 9 | import { StyledNodeWrapper } from './node.styled'; 10 | 11 | export const GrpcServiceNode: React.FC> = ({ 12 | data, 13 | isOpen, 14 | onCollapseToggle, 15 | }) => { 16 | const content = ( 17 | 18 | 19 | 20 | 21 | {data.name} 22 | 23 | 24 | ); 25 | 26 | return ( 27 | 35 | data={data.methods}> 36 | {data.methods?.map((method) => ( 37 | 38 | ))} 39 | 40 | 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /src/app/components/color-picker/color-picker.input.tsx: -------------------------------------------------------------------------------- 1 | import { faFill } from '@fortawesome/free-solid-svg-icons'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { Button, Spacer, styled } from '@nextui-org/react'; 4 | import React from 'react'; 5 | 6 | import { ColorCircle } from '../color-circle'; 7 | import { ColorPicker } from './color-picker'; 8 | 9 | const StyledWrapper = styled('div', { 10 | display: 'flex', 11 | flexWrap: 'nowrap', 12 | alignItems: 'center', 13 | }); 14 | 15 | export interface ColorPickerInputProps { 16 | value: string; 17 | 18 | onChange: (color: string) => void; 19 | } 20 | 21 | export const ColorPickerInput: React.FC = ({ value, onChange }) => { 22 | const [colorPickerVisible, setColorPickerVisible] = React.useState(false); 23 | 24 | return ( 25 | 26 | 27 | 28 | } css={{ minWidth: 10 }} /> 31 | } 32 | isOpen={colorPickerVisible} 33 | onOpenChange={(isVisible) => setColorPickerVisible(isVisible)} 34 | color={value} 35 | onColorChange={(newColor) => onChange(newColor.hex)} 36 | /> 37 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/app/components/color-circle/color-circle.tsx: -------------------------------------------------------------------------------- 1 | import { styled, VariantProps } from '@nextui-org/react'; 2 | import chroma from 'chroma-js'; 3 | import React from 'react'; 4 | 5 | const StyledCircle = styled('div', { 6 | minHeight: 'calc($$circleHeightRatio * $3)', 7 | minWidth: 'calc($$circleWidthRatio * $3)', 8 | borderRadius: '50%', 9 | 10 | variants: { 11 | size: { 12 | xs: { 13 | $$circleHeightRatio: '1.2', 14 | $$circleWidthRatio: '1.2', 15 | }, 16 | sm: { 17 | $$circleHeightRatio: '1.6', 18 | $$circleWidthRatio: '1.6', 19 | }, 20 | md: { 21 | $$circleHeightRatio: '2', 22 | $$circleWidthRatio: '2', 23 | }, 24 | lg: { 25 | $$circleHeightRatio: '2.2', 26 | $$circleWidthRatio: '2.2', 27 | }, 28 | xl: { 29 | $$circleHeightRatio: '2.6', 30 | $$circleWidthRatio: '2.6', 31 | }, 32 | }, 33 | }, 34 | }); 35 | 36 | export type CircleProps = { 37 | color: string; 38 | shadow?: boolean; 39 | } & VariantProps; 40 | 41 | export const ColorCircle: React.FC = ({ color, size = 'sm', shadow = false }) => ( 42 | 49 | ); 50 | -------------------------------------------------------------------------------- /src/app/components/tabs/tabs.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from '@nextui-org/react'; 2 | import React, { PropsWithChildren } from 'react'; 3 | 4 | import { TabProps } from './tab'; 5 | import { TabBar, TabBarProps } from './tab-bar'; 6 | import { TabContent } from './tab-content'; 7 | 8 | const StyledTabs = styled('div', { 9 | display: 'flex', 10 | flexDirection: 'column', 11 | flex: 1, 12 | 13 | overflow: 'hidden', 14 | }); 15 | 16 | export type TabsProps = PropsWithChildren; 17 | 18 | export const Tabs: React.FC = ({ 19 | children, 20 | activeBar, 21 | activeKey, 22 | draggable, 23 | rightNode, 24 | onTabActivate, 25 | onTabClose, 26 | onTabDragEnd, 27 | }) => { 28 | const tabs = React.Children.toArray(children) as React.ReactElement[]; 29 | 30 | const tabsContents = tabs.map((tab) => ( 31 | 32 | {tab} 33 | 34 | )); 35 | 36 | return ( 37 | 38 | 47 | {children} 48 | 49 | {tabsContents} 50 | 51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /src/app/components/code-editor/code-editor.styled.ts: -------------------------------------------------------------------------------- 1 | import { styled } from '@nextui-org/react'; 2 | import CodeMirror from '@uiw/react-codemirror'; 3 | 4 | export const StyledCodeMirror = styled(CodeMirror, { 5 | '&': { 6 | color: '$text', 7 | backgroundColor: '$background', 8 | width: '100%', 9 | }, 10 | 11 | '.cm-content': { 12 | padding: 0, 13 | fontFamily: '$mono', 14 | fontSize: '$fontSizes$xs', 15 | }, 16 | 17 | '.cm-editor.cm-focused': { 18 | outline: 'none', 19 | }, 20 | 21 | '.cm-cursor, .cm-dropCursor': { 22 | borderLeftColor: '$text', 23 | }, 24 | 25 | '.cm-focused .cm-selectionBackground, .cm-selectionBackground': { 26 | backgroundColor: '$selection', 27 | }, 28 | 29 | '.cm-activeLine': { 30 | backgroundColor: '$activeLine', 31 | }, 32 | 33 | '.cm-selectionMatch': { 34 | backgroundColor: '$green200', 35 | }, 36 | 37 | '.cm-line': { 38 | padding: '0 0 0 4px', 39 | }, 40 | 41 | '.cm-gutters': { 42 | backgroundColor: '$background', 43 | color: '$accents8', 44 | border: 'none', 45 | userSelect: 'none', 46 | fontFamily: '$mono', 47 | fontSize: '$fontSizes$sm', 48 | }, 49 | 50 | '.cm-activeLineGutter': { 51 | backgroundColor: '$accents1', 52 | }, 53 | 54 | '.cm-foldPlaceholder': { 55 | backgroundColor: 'transparent', 56 | border: 'none', 57 | color: '$yellow500', 58 | }, 59 | }); 60 | -------------------------------------------------------------------------------- /src/app/pages/tabs-container/collection-types/grpc/response/streams/client-streaming.response.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Tab, Tabs, Tree } from '@components'; 4 | import { GrpcMethodType } from '@core/types'; 5 | import { GrpcStreamMessage, GrpcTab } from '@storage'; 6 | 7 | import { EmptyResponseView } from '../empty-response-view'; 8 | import { ReponseNode } from './response.node'; 9 | import { ListWrapper, StyledContainer } from './response.styled'; 10 | 11 | export interface ClientStreamingResponseProps { 12 | tab: GrpcTab; 13 | } 14 | 15 | export const ClientStreamingResponse: React.FC = ({ tab }) => ( 16 | 17 | 18 | 19 | {tab.data.response.messages ? ( 20 | 21 | data={tab.data.response.messages} defaultIsOpen={false}> 22 | {tab.data.response.messages?.map((message) => ( 23 | 24 | ))} 25 | 26 | 27 | ) : ( 28 | 29 | )} 30 | 31 | 32 | 33 | ); 34 | -------------------------------------------------------------------------------- /src/app/pages/tabs-container/collection-types/grpc/response/streams/server-streaming.response.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Tab, Tabs, Tree } from '@components'; 4 | import { GrpcMethodType } from '@core/types'; 5 | import { GrpcStreamMessage, GrpcTab } from '@storage'; 6 | 7 | import { EmptyResponseView } from '../empty-response-view'; 8 | import { ReponseNode } from './response.node'; 9 | import { ListWrapper, StyledContainer } from './response.styled'; 10 | 11 | export interface ServerStreamingResponseProps { 12 | tab: GrpcTab; 13 | } 14 | 15 | export const ServerStreamingResponse: React.FC = ({ tab }) => ( 16 | 17 | 18 | 19 | {tab.data.response.messages ? ( 20 | 21 | data={tab.data.response.messages} defaultIsOpen={false}> 22 | {tab.data.response.messages?.map((message) => ( 23 | 24 | ))} 25 | 26 | 27 | ) : ( 28 | 29 | )} 30 | 31 | 32 | 33 | ); 34 | -------------------------------------------------------------------------------- /src/main/clients/grpc-client/subscribers/index.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, ipcMain } from 'electron'; 2 | 3 | import { GrpcClientBidirectionalSubscriber } from './bidirectional-streaming.subscriber'; 4 | import { GrpcClientClientStreamingSubscriber } from './client-streaming.subscriber'; 5 | import { GrpcClientServerStreamingSubscriber } from './server-streaming.subscriber'; 6 | import { GrpcClientUnarySubscriber } from './unary.subscriber'; 7 | 8 | export const registerGrpcClientSubscribers = (mainWindow: BrowserWindow) => { 9 | const bidirectional = new GrpcClientBidirectionalSubscriber(mainWindow, ipcMain); 10 | const client = new GrpcClientClientStreamingSubscriber(mainWindow, ipcMain); 11 | const server = new GrpcClientServerStreamingSubscriber(mainWindow, ipcMain); 12 | const unary = new GrpcClientUnarySubscriber(mainWindow, ipcMain); 13 | 14 | bidirectional.registerBidirectionalStreamingHandlers(); 15 | client.registerClientStreamingHandlers(); 16 | server.registerServerStreamingHandlers(); 17 | unary.registerUnaryCallHandlers(); 18 | }; 19 | 20 | export const unregisterGrpcClientSubscribers = () => { 21 | GrpcClientBidirectionalSubscriber.unregisterBidirectionalStreamingHandlers(ipcMain); 22 | GrpcClientClientStreamingSubscriber.unregisterClientStreamingHandlers(ipcMain); 23 | GrpcClientServerStreamingSubscriber.unregisterServerStreamingHandlers(ipcMain); 24 | GrpcClientUnarySubscriber.unregisterUnaryCallHandlers(ipcMain); 25 | }; 26 | -------------------------------------------------------------------------------- /src/app/pages/tabs-container/collection-types/grpc/response/streams/bidirectional-streaming.response.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Tab, Tabs, Tree } from '@components'; 4 | import { GrpcMethodType } from '@core/types'; 5 | import { GrpcStreamMessage, GrpcTab } from '@storage'; 6 | 7 | import { EmptyResponseView } from '../empty-response-view'; 8 | import { ReponseNode } from './response.node'; 9 | import { ListWrapper, StyledContainer } from './response.styled'; 10 | 11 | export interface BidirectionalStreamingResponseProps { 12 | tab: GrpcTab; 13 | } 14 | 15 | export const BidirectionalStreamingResponse: React.FC = ({ 16 | tab, 17 | }) => ( 18 | 19 | 20 | 21 | {tab.data.response.messages ? ( 22 | 23 | data={tab.data.response.messages} defaultIsOpen={false}> 24 | {tab.data.response.messages?.map((message) => ( 25 | 26 | ))} 27 | 28 | 29 | ) : ( 30 | 31 | )} 32 | 33 | 34 | 35 | ); 36 | -------------------------------------------------------------------------------- /src/app/components/tree/tree.tsx: -------------------------------------------------------------------------------- 1 | import { CSS, styled } from '@nextui-org/react'; 2 | import React, { PropsWithChildren } from 'react'; 3 | 4 | import { TreeNodeProps } from './tree-node'; 5 | 6 | const StyledTree = styled('ul', { 7 | margin: 0, 8 | }); 9 | 10 | export type TreeData = { 11 | id: string; 12 | }; 13 | 14 | export type TreeProps = { 15 | css?: CSS; 16 | 17 | data?: T[]; 18 | 19 | defaultIsOpen?: boolean; 20 | }; 21 | 22 | export const Tree: ( 23 | props: PropsWithChildren> 24 | ) => React.ReactElement> = ({ css, data = [], defaultIsOpen = true, children }) => { 25 | const isOpenDefaultState = data.reduce( 26 | (acc, item) => ({ 27 | ...acc, 28 | [item.id]: defaultIsOpen, 29 | }), 30 | {} 31 | ); 32 | 33 | const [isOpen, setIsOpen] = React.useState<{ [key: string]: boolean }>(isOpenDefaultState); 34 | 35 | const handleIsOpen = (id: string) => (value: boolean) => { 36 | setIsOpen({ 37 | ...isOpen, 38 | [id]: value, 39 | }); 40 | }; 41 | 42 | const nodes = (React.Children.toArray(children) as React.ReactElement[]).map( 43 | (node) => 44 | React.cloneElement(node, { 45 | isOpen: isOpen[node.props.id] !== undefined ? isOpen[node.props.id] : defaultIsOpen, 46 | onCollapseToggle: handleIsOpen(node.props.id), 47 | }) 48 | ); 49 | 50 | return {nodes}; 51 | }; 52 | -------------------------------------------------------------------------------- /src/app/components/tabs/tab-bar/active-bar.tsx: -------------------------------------------------------------------------------- 1 | import { CSS, styled, VariantProps } from '@nextui-org/react'; 2 | import React from 'react'; 3 | 4 | const StyledActiveBar = styled('div', { 5 | position: 'absolute', 6 | height: 1, 7 | pointerEvents: 'none', 8 | zIndex: 2, 9 | 10 | variants: { 11 | color: { 12 | primary: { 13 | background: '$primary', 14 | }, 15 | secondary: { 16 | background: '$secondary', 17 | }, 18 | success: { 19 | background: '$success', 20 | }, 21 | warning: { 22 | background: '$warning', 23 | }, 24 | error: { 25 | background: '$error', 26 | }, 27 | }, 28 | 29 | disabled: { 30 | true: { 31 | display: 'none', 32 | }, 33 | }, 34 | 35 | position: { 36 | top: { 37 | top: 1, 38 | }, 39 | bottom: { 40 | bottom: 0, 41 | }, 42 | }, 43 | 44 | animated: { 45 | true: { 46 | transition: 'all 0.2s', 47 | }, 48 | }, 49 | }, 50 | }); 51 | 52 | export type ActiveBarProps = { css?: CSS } & VariantProps; 53 | 54 | export const ActiveBar: React.FC = ({ 55 | disabled = false, 56 | position = 'top', 57 | animated = true, 58 | color = 'primary', 59 | css, 60 | }) => ( 61 | 68 | ); 69 | -------------------------------------------------------------------------------- /src/app/components/icons/vertical-layout.icon.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@nextui-org/react'; 2 | import React from 'react'; 3 | 4 | export const VerticalLayoutIcon: React.FC> = ({ onClick }) => { 5 | const { theme } = useTheme(); 6 | 7 | const [isHovered, setIsHovered] = React.useState(false); 8 | 9 | return ( 10 | setIsHovered(true)} 18 | onMouseLeave={() => setIsHovered(false)} 19 | > 20 | 26 | 32 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /src/app/pages/status-bar.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Text } from '@nextui-org/react'; 2 | import React from 'react'; 3 | 4 | import { HorizontalLayoutIcon, VerticalLayoutIcon } from '@components'; 5 | import { Alignment, useSettingsStore } from '@storage'; 6 | 7 | import packageJson from '../../../package.json'; 8 | 9 | export const StatusBar: React.FC = () => { 10 | const { alignment, updateAlignment } = useSettingsStore((store) => store); 11 | 12 | const handleAlignmentChange = (newAlignment: Alignment) => { 13 | updateAlignment(newAlignment); 14 | }; 15 | 16 | return ( 17 | 26 | 27 | 28 | {packageJson.version} 29 | 30 | 31 | 32 | {alignment === Alignment.VERTICAL ? ( 33 | handleAlignmentChange(Alignment.HORIZONTAL)} /> 34 | ) : ( 35 | handleAlignmentChange(Alignment.VERTICAL)} /> 36 | )} 37 | 38 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: ['./tsconfig.json', './__tests__/simple-service/tsconfig.json', './__tests__/tls-service/tsconfig.json'] 5 | }, 6 | plugins: [ 7 | 'simple-import-sort', 8 | 'prettier' 9 | ], 10 | extends: [ 11 | 'airbnb', 12 | 'airbnb/hooks', 13 | 'airbnb-typescript', 14 | 'plugin:import/electron', 15 | 'prettier', 16 | 'plugin:storybook/recommended' 17 | ], 18 | env: { 19 | browser: true, 20 | es6: true, 21 | node: true 22 | }, 23 | rules: { 24 | 'prettier/prettier': ['error', { 25 | printWidth: 100, 26 | singleQuote: true, 27 | trailingComma: 'es5' 28 | }], 29 | 30 | 'import/prefer-default-export': 'off', 31 | 'import/no-extraneous-dependencies': [ 32 | 'error', 33 | { 34 | 'devDependencies': [ 35 | '**/*.stories.*', 36 | '**/.storybook/**/*.*' 37 | ], 38 | 'peerDependencies': true 39 | } 40 | ], 41 | 42 | 'react/prop-types': 'off', 43 | 'react/function-component-definition': 'off', 44 | 'react/require-default-props': 'off', 45 | 'react/jsx-props-no-spreading': 'off', 46 | 47 | 'react-hooks/exhaustive-deps': 'off', 48 | 49 | 'simple-import-sort/imports': [ 50 | 'error', 51 | { 'groups': [['^\\u0000'], ['^[^.]'], ['^@(context|hooks|layouts|components|storage|core|core/types)'], ['^\\.'], ['^.+\\.s?css$']] } 52 | ], 53 | 54 | 'no-plusplus': 'off' 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /src/app/components/menu/menu-item.tsx: -------------------------------------------------------------------------------- 1 | import { styled, VariantProps } from '@nextui-org/react'; 2 | import React from 'react'; 3 | 4 | const StyledMenuItem = styled('div', { 5 | display: 'flex', 6 | justifyContent: 'center', 7 | padding: 10, 8 | 9 | cursor: 'pointer', 10 | 11 | '&:hover': { 12 | color: '$ezy', 13 | }, 14 | 15 | variants: { 16 | active: { 17 | true: { 18 | color: '$ezy', 19 | }, 20 | false: { 21 | color: '$accents5', 22 | }, 23 | }, 24 | }, 25 | }); 26 | 27 | const IconWrapper = styled('div', { 28 | display: 'flex', 29 | justifyContent: 'center', 30 | alignItems: 'center', 31 | 32 | width: 30, 33 | height: 30, 34 | 35 | variants: { 36 | active: { 37 | true: { 38 | transition: 'all 0.2s ease', 39 | bs: '$xs', 40 | br: '$squared', 41 | backgroundColor: '$accents1', 42 | }, 43 | false: { 44 | backgroundColor: 'transparent', 45 | }, 46 | }, 47 | }, 48 | }); 49 | 50 | export type MenuItemData = { 51 | id: string; 52 | 53 | icon: React.ReactElement; 54 | 55 | // eslint-disable-next-line react/no-unused-prop-types 56 | submenu?: React.ReactElement; 57 | }; 58 | 59 | export type MenuItemProps = MenuItemData & { 60 | onClick?: () => void; 61 | } & VariantProps; 62 | 63 | export const MenuItem: React.FC = ({ id, icon, active = false, onClick }) => ( 64 | 65 | {icon} 66 | 67 | ); 68 | -------------------------------------------------------------------------------- /src/app/components/select/colored/colored-single-value.tsx: -------------------------------------------------------------------------------- 1 | import { Spacer, styled, VariantProps } from '@nextui-org/react'; 2 | import React from 'react'; 3 | import { components, SingleValueProps } from 'react-select'; 4 | 5 | import { ColorCircle } from '../../color-circle'; 6 | import { ColoredSelectOption } from './interfaces'; 7 | 8 | const StyledSpan = styled('span', { 9 | display: 'flex', 10 | flexWrap: 'nowrap', 11 | alignItems: 'center', 12 | gap: 5, 13 | 14 | userSelect: 'none', 15 | font: 'inherit', 16 | fontSize: '$$selectFontSize', 17 | 18 | variants: { 19 | size: { 20 | xs: { 21 | $$selectFontSize: '$fontSizes$xs', 22 | }, 23 | sm: { 24 | $$selectFontSize: '$fontSizes$xs', 25 | }, 26 | md: { 27 | $$selectFontSize: '$fontSizes$xs', 28 | }, 29 | lg: { 30 | $$selectFontSize: '$fontSizes$base', 31 | }, 32 | xl: { 33 | $$selectFontSize: '$fontSizes$sm', 34 | }, 35 | }, 36 | }, 37 | }); 38 | 39 | export type ColoredSingleValueProps = SingleValueProps & { 40 | selectProps: VariantProps; 41 | }; 42 | 43 | export const ColoredSingleValue: React.FC = ({ 44 | data, 45 | selectProps, 46 | ...props 47 | }) => ( 48 | 49 | 50 | 51 | 52 | {data.label} 53 | 54 | 55 | ); 56 | -------------------------------------------------------------------------------- /src/app/pages/collections/forms/fields/include-directories/directory.node.tsx: -------------------------------------------------------------------------------- 1 | import { faTrash } from '@fortawesome/free-solid-svg-icons'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { Button, Container, Row, Text } from '@nextui-org/react'; 4 | import React from 'react'; 5 | 6 | import { TreeData, TreeNode, TreeNodeRendererProps } from '@components'; 7 | 8 | export type DirectoryNodeData = TreeData & { 9 | value: string; 10 | }; 11 | 12 | export const DirectoryNode: React.FC< 13 | TreeNodeRendererProps & { onDirectoryRemove: (id: string) => void } 14 | > = ({ data, onDirectoryRemove }) => { 15 | const content = ( 16 | 17 | 18 | {data.value} 19 | 20 | 21 | ); 22 | 23 | const handleRemoveButtonClick = () => { 24 | onDirectoryRemove(data.id); 25 | }; 26 | 27 | const commandsContent = ( 28 | 42 | 53 | 54 | 55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /src/app/storage/interfaces/tabs/grpc-tab.interface.ts: -------------------------------------------------------------------------------- 1 | import { GrpcMethodType } from '@core/types'; 2 | 3 | export enum GrpcProtocol { 4 | GRPC = 'grpc', 5 | GRPC_WEB = 'grpc-web', 6 | } 7 | 8 | export interface GrpcRequest { 9 | id: string; 10 | value?: string; 11 | } 12 | 13 | export interface GrpcRequestMetadata { 14 | id: string; 15 | value?: string; 16 | } 17 | 18 | export interface GrpcUnaryResponse { 19 | id: string; 20 | code: number; 21 | timestamp?: number; 22 | value?: string; 23 | } 24 | 25 | export enum GrpcStreamMessageType { 26 | STARTED = 'started', 27 | CLIENT_MESSAGE = 'client-message', 28 | SERVER_MESSAGE = 'server-message', 29 | ERROR = 'error', 30 | CLIENT_STREAMING_ENDED = 'client-streaming-ended', 31 | SERVER_STREAMING_ENDED = 'server-streaming-ended', 32 | CANCELED = 'canceled', 33 | } 34 | 35 | export interface GrpcStreamMessage { 36 | id: string; 37 | type: GrpcStreamMessageType; 38 | timestamp: number; 39 | value?: string; 40 | } 41 | 42 | export interface GrpcStreamResponse { 43 | id: string; 44 | messages?: GrpcStreamMessage[]; 45 | } 46 | 47 | export interface GrpcTabInfo { 48 | collectionId: string; 49 | serviceId: string; 50 | methodId: string; 51 | methodType: T; 52 | } 53 | 54 | export type GrpcResponse = T extends GrpcMethodType.UNARY 55 | ? GrpcUnaryResponse 56 | : GrpcStreamResponse; 57 | 58 | export interface GrpcTabData { 59 | protocol: GrpcProtocol; 60 | environmentId?: string; 61 | url?: string; 62 | tlsId?: string; 63 | 64 | requestTabs: { 65 | activeTabId: string | undefined; 66 | request: GrpcRequest; 67 | metadata: GrpcRequestMetadata; 68 | }; 69 | 70 | response: GrpcResponse; 71 | } 72 | -------------------------------------------------------------------------------- /stories/Select.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Grid, Spacer, Text } from '@nextui-org/react'; 2 | import { ComponentMeta, ComponentStory } from '@storybook/react'; 3 | import React from 'react'; 4 | 5 | import { Select as SelectComponent } from '../src/app/components'; 6 | 7 | // More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 8 | export default { 9 | title: 'Select', 10 | component: SelectComponent, 11 | // More on argTypes: https://storybook.js.org/docs/react/api/argtypes 12 | argTypes: {}, 13 | } as ComponentMeta; 14 | 15 | // More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args 16 | const Template: ComponentStory = () => ( 17 | 18 | Default 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | Bordered 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | ); 49 | 50 | export const Select = Template.bind({}); 51 | -------------------------------------------------------------------------------- /src/app/hooks/use-shortcuts.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { AppContext } from '@context'; 4 | 5 | export enum ShortcutsGroup { 6 | WELCOME = 'welcome', 7 | RESPONSE = 'response', 8 | } 9 | 10 | const shortcuts = [ 11 | { 12 | key: { 13 | darwin: '⌘+K', 14 | other: 'Ctrl+K', 15 | }, 16 | description: 'Open command bar', 17 | groups: [ShortcutsGroup.WELCOME], 18 | }, 19 | { 20 | key: { 21 | darwin: '⌘+Shift+C', 22 | other: 'Ctrl+Shift+C', 23 | }, 24 | description: 'Create new collection', 25 | groups: [ShortcutsGroup.WELCOME], 26 | }, 27 | { 28 | key: { 29 | darwin: '⌘+Shift+S', 30 | other: 'Ctrl+Shift+S', 31 | }, 32 | description: 'Synchronize collections', 33 | groups: [ShortcutsGroup.WELCOME], 34 | }, 35 | { 36 | key: { 37 | darwin: '⌘+T', 38 | other: 'Ctrl+T', 39 | }, 40 | description: 'New gRPC tab', 41 | groups: [ShortcutsGroup.WELCOME], 42 | }, 43 | { 44 | key: { 45 | darwin: '⌘+Enter', 46 | other: 'Ctrl+Enter', 47 | }, 48 | description: 'Invoke request', 49 | groups: [ShortcutsGroup.RESPONSE], 50 | }, 51 | ]; 52 | 53 | export function useShortcuts() { 54 | const context = React.useContext(AppContext); 55 | 56 | const shortcutsMemoized = React.useMemo( 57 | () => 58 | shortcuts.map((shortcut) => ({ 59 | key: context?.platform.os === 'darwin' ? shortcut.key.darwin : shortcut.key.other, 60 | description: shortcut.description, 61 | groups: shortcut.groups, 62 | })), 63 | [context?.platform.os] 64 | ); 65 | 66 | function getShortcuts(group: ShortcutsGroup) { 67 | return shortcutsMemoized.filter((shortcut) => shortcut.groups.includes(group)); 68 | } 69 | 70 | return { getShortcuts }; 71 | } 72 | -------------------------------------------------------------------------------- /src/app/pages/shortcuts/hooks/use-environment-actions.tsx: -------------------------------------------------------------------------------- 1 | import { faBolt } from '@fortawesome/free-solid-svg-icons'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { Action, Priority, useRegisterActions } from '@getezy/kbar'; 4 | import { Container } from '@nextui-org/react'; 5 | import React from 'react'; 6 | 7 | import { ColorCircle } from '@components'; 8 | import { useEnvironmentsStore, useTabsStore } from '@storage'; 9 | 10 | export function useEnvironmentActions() { 11 | const { environments } = useEnvironmentsStore((store) => store); 12 | const { activeTabId, updateGrpcTabData } = useTabsStore((store) => store); 13 | 14 | const actions: Action[] = environments.map((environment) => ({ 15 | id: environment.id, 16 | name: environment.label, 17 | keywords: environment.label, 18 | subtitle: environment.url, 19 | parent: 'environment', 20 | icon: ( 21 | 29 | 30 | 31 | ), 32 | perform: () => { 33 | if (activeTabId) { 34 | updateGrpcTabData(activeTabId, { 35 | environmentId: environment.id, 36 | url: environment.url, 37 | }); 38 | } 39 | }, 40 | })); 41 | 42 | useRegisterActions( 43 | [ 44 | { 45 | id: 'environment', 46 | section: 'Environment', 47 | name: 'Select environment', 48 | priority: Priority.HIGH, 49 | icon: , 50 | shortcut: ['$mod+E'], 51 | }, 52 | ...actions, 53 | ], 54 | [actions] 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /src/app/components/kbar/kbar.styled.ts: -------------------------------------------------------------------------------- 1 | import { KBarAnimator, KBarPositioner, KBarSearch } from '@getezy/kbar'; 2 | import { styled } from '@nextui-org/react'; 3 | 4 | export const StyledKBarPositioner = styled(KBarPositioner, { 5 | zIndex: '$max', 6 | }); 7 | 8 | export const StyledKBarAnimator = styled(KBarAnimator, { 9 | maxWidth: '600px', 10 | width: '100%', 11 | background: '$backgroundContrast', 12 | color: '$text', 13 | borderRadius: '8px', 14 | overflow: 'hidden', 15 | boxShadow: '$xl', 16 | }); 17 | 18 | export const StyledKBarSearch = styled(KBarSearch, { 19 | padding: '12px 16px', 20 | fontFamily: '$sans', 21 | fontSize: '16px', 22 | width: '100%', 23 | boxSizing: 'border-box', 24 | outline: 'none', 25 | border: 'none', 26 | background: '$backgroundContrast', 27 | color: '$text', 28 | }); 29 | 30 | export const StyledGroupName = styled('div', { 31 | padding: '8px 16px', 32 | fontSize: '10px', 33 | textTransform: 'uppercase', 34 | opacity: 0.5, 35 | }); 36 | 37 | export const StyledResultItem = styled('div', { 38 | padding: '12px 16px', 39 | display: 'flex', 40 | alignItems: 'center', 41 | justifyContent: 'space-between', 42 | cursor: 'pointer', 43 | 44 | variants: { 45 | active: { 46 | true: { 47 | backgroundColor: '$accents1', 48 | borderLeft: '2px solid $foreground', 49 | }, 50 | false: { 51 | backgroundColor: 'transparent', 52 | borderLeft: '2px solid transparent', 53 | }, 54 | }, 55 | }, 56 | }); 57 | 58 | export const StyledActionWrapper = styled('div', { 59 | display: 'flex', 60 | gap: '8px', 61 | alignItems: 'center', 62 | fontSize: 14, 63 | }); 64 | 65 | export const StyledShortcutWrapper = styled('div', { 66 | display: 'grid', 67 | gridAutoFlow: 'column', 68 | gap: '1px', 69 | }); 70 | -------------------------------------------------------------------------------- /__tests__/tls-service/src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import { Server, ServerCredentials } from '@grpc/grpc-js'; 4 | import * as fs from 'fs'; 5 | import * as path from 'path'; 6 | 7 | import { TLSServiceServer, TLSServiceService } from './generated/proto/tls_service'; 8 | 9 | const TLSService: TLSServiceServer = { 10 | unary(call, callback) { 11 | callback(null, call.request); 12 | }, 13 | }; 14 | 15 | function getServerCredentials(): ServerCredentials { 16 | const serverCert = fs.readFileSync(path.resolve(__dirname, '../certs/server-cert.pem')); 17 | const serverKey = fs.readFileSync(path.resolve(__dirname, '../certs/server-key.pem')); 18 | 19 | if (process.env.TLS_MODE === 'mutual') { 20 | const rootCert = fs.readFileSync(path.resolve(__dirname, '../certs/ca-cert.pem')); 21 | 22 | const serverCredentials = ServerCredentials.createSsl( 23 | rootCert, 24 | [ 25 | { 26 | cert_chain: serverCert, 27 | private_key: serverKey, 28 | }, 29 | ], 30 | true 31 | ); 32 | 33 | console.log('Using mutual TLS'); 34 | return serverCredentials; 35 | } 36 | 37 | const serverCredentials = ServerCredentials.createSsl( 38 | null, 39 | [ 40 | { 41 | cert_chain: serverCert, 42 | private_key: serverKey, 43 | }, 44 | ], 45 | false 46 | ); 47 | 48 | console.log('Using server side TLS'); 49 | return serverCredentials; 50 | } 51 | 52 | function main() { 53 | const server = new Server(); 54 | 55 | const serverCredentials = getServerCredentials(); 56 | 57 | server.addService(TLSServiceService, TLSService); 58 | 59 | server.bindAsync('0.0.0.0:4001', serverCredentials, () => { 60 | server.start(); 61 | 62 | console.log('gRPC server started on 0.0.0.0:4001'); 63 | }); 64 | } 65 | 66 | main(); 67 | -------------------------------------------------------------------------------- /stories/ColoredSelect.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Grid, Spacer, Text } from '@nextui-org/react'; 2 | import { ComponentMeta, ComponentStory } from '@storybook/react'; 3 | import React from 'react'; 4 | 5 | import { ColoredSelect as ColoredSelectComponent } from '../src/app/components'; 6 | 7 | // More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 8 | export default { 9 | title: 'ColoredSelect', 10 | component: ColoredSelectComponent, 11 | // More on argTypes: https://storybook.js.org/docs/react/api/argtypes 12 | argTypes: {}, 13 | } as ComponentMeta; 14 | 15 | // More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args 16 | const Template: ComponentStory = () => ( 17 | 18 | Default 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | Bordered 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | ); 49 | 50 | export const ColoredSelect = Template.bind({}); 51 | -------------------------------------------------------------------------------- /src/app/storage/tls-presets.storage.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | 3 | import { produce } from 'immer'; 4 | import { nanoid } from 'nanoid'; 5 | import create from 'zustand'; 6 | import { persist } from 'zustand/middleware'; 7 | 8 | import { GrpcTlsType } from '@core/types'; 9 | 10 | import { TlsPresetsStorage } from './interfaces'; 11 | 12 | export const useTlsPresetsStore = create( 13 | persist( 14 | (set) => ({ 15 | presets: [ 16 | { 17 | id: nanoid(), 18 | name: 'Insecure', 19 | system: true, 20 | tls: { type: GrpcTlsType.INSECURE }, 21 | }, 22 | { 23 | id: nanoid(), 24 | name: 'Server-side', 25 | system: true, 26 | tls: { type: GrpcTlsType.SERVER_SIDE }, 27 | }, 28 | ], 29 | createTlsPreset: (preset) => 30 | set( 31 | produce((state) => { 32 | state.presets.push({ 33 | system: false, 34 | ...preset, 35 | }); 36 | }) 37 | ), 38 | updateTlsPreset: (id, preset) => 39 | set( 40 | produce((state) => { 41 | const index = state.presets.findIndex((item) => item.id === id); 42 | 43 | if (index !== -1) { 44 | state.presets[index] = { 45 | ...state.presets[index], 46 | ...preset, 47 | }; 48 | } 49 | }) 50 | ), 51 | removeTlsPreset: (id) => 52 | set( 53 | produce((state) => { 54 | const index = state.presets.findIndex((preset) => preset.id === id); 55 | if (index !== -1) state.presets.splice(index, 1); 56 | }) 57 | ), 58 | }), 59 | { 60 | name: 'tls-presets', 61 | getStorage: () => window.electronStore, 62 | } 63 | ) 64 | ); 65 | -------------------------------------------------------------------------------- /src/core/clients/grpc/grpc-web-client/grpc-web-call.stream.ts: -------------------------------------------------------------------------------- 1 | import { grpc } from '@improbable-eng/grpc-web'; 2 | import { EventEmitter } from 'events'; 3 | import * as https from 'https'; 4 | 5 | import { GrpcWebError } from './grpc-web.error'; 6 | import { NodeHttpTransport } from './http.transport'; 7 | 8 | export declare interface GrpcWebCallStream { 9 | on(event: 'message', listener: (message: Record) => void): this; 10 | on(event: 'headers', listener: (headers: grpc.Metadata) => void): this; 11 | on(event: 'error', listener: (error: GrpcWebError) => void): this; 12 | on( 13 | event: 'end', 14 | listener: (code: grpc.Code, details: string, metadata: grpc.Metadata) => void 15 | ): this; 16 | } 17 | 18 | export class GrpcWebCallStream extends EventEmitter { 19 | private call: grpc.Request; 20 | 21 | constructor( 22 | private readonly methodDefinition: grpc.MethodDefinition< 23 | grpc.ProtobufMessage, 24 | grpc.ProtobufMessage 25 | >, 26 | private readonly options: Omit< 27 | grpc.InvokeRpcOptions, 28 | 'onMessage' | 'onEnd' | 'transport' 29 | >, 30 | private readonly httpsOptions?: https.RequestOptions 31 | ) { 32 | super(); 33 | 34 | this.call = grpc.invoke(this.methodDefinition, { 35 | ...this.options, 36 | transport: NodeHttpTransport({ 37 | ...httpsOptions, 38 | }), 39 | onMessage: (responseMessage) => { 40 | this.emit('message', responseMessage); 41 | }, 42 | onHeaders: (headers) => { 43 | this.emit('headers', headers); 44 | }, 45 | onEnd: (code, details, metadata) => { 46 | if (code !== grpc.Code.OK) { 47 | this.emit('error', new GrpcWebError(code, details, metadata)); 48 | } else { 49 | this.emit('end', code, details, metadata); 50 | } 51 | }, 52 | }); 53 | } 54 | 55 | cancel() { 56 | this.call.close(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/app/pages/collections/modals/update-collection.modal.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Modal, ModalProps, Spacer, Text } from '@nextui-org/react'; 2 | import React from 'react'; 3 | 4 | import { Badge } from '@components'; 5 | import { useUpdateCollection } from '@hooks'; 6 | import { Collection, CollectionType } from '@storage'; 7 | 8 | import { CollectionForm } from '../forms'; 9 | 10 | export type UpdateCollectionModalProps = ModalProps & { 11 | defaultValues?: Partial>; 12 | }; 13 | 14 | export const UpdateCollectionModal: React.FC = ({ 15 | onClose = () => {}, 16 | defaultValues, 17 | ...props 18 | }) => { 19 | const { update } = useUpdateCollection(); 20 | 21 | const handleSubmit = async (payload: Collection) => { 22 | await update(payload.id, payload); 23 | 24 | onClose(); 25 | }; 26 | 27 | return ( 28 | 35 | 36 | 37 | Update Collection 38 | 39 | 40 | 41 | 42 | 47 | 48 | 49 | 52 | 62 | 63 | 64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /src/app/pages/tabs-container/collection-types/grpc/environments/create-environment.modal.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Modal, ModalProps, Text } from '@nextui-org/react'; 2 | import { nanoid } from 'nanoid'; 3 | import React from 'react'; 4 | 5 | import { Environment, useEnvironmentsStore } from '@storage'; 6 | 7 | import { EnvironmentForm } from './environment.form'; 8 | 9 | export type CreateEnvironmentModalProps = ModalProps & { 10 | defaultValues?: Partial>; 11 | onCreate: (environment: Environment) => void; 12 | }; 13 | 14 | export const CreateEnvironmentModal: React.FC = ({ 15 | onCreate, 16 | onClose = () => {}, 17 | defaultValues, 18 | ...props 19 | }) => { 20 | const createEnvironment = useEnvironmentsStore((store) => store.createEnvironment); 21 | 22 | const handleSubmit = (payload: Environment) => { 23 | const environment: Environment = { ...payload, id: nanoid() }; 24 | 25 | createEnvironment(environment); 26 | onCreate(environment); 27 | }; 28 | 29 | return ( 30 | 36 | 37 | New Environment 38 | 39 | 40 | 45 | 46 | 47 | 50 | 60 | 61 | 62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /src/app/pages/side-bar/collections-tree.tsx: -------------------------------------------------------------------------------- 1 | import { Container, FormElement, Input, Spacer, styled, Text } from '@nextui-org/react'; 2 | import React from 'react'; 3 | 4 | import { Kbd, Tree } from '@components'; 5 | import { Collection, CollectionType, useCollectionsStore } from '@storage'; 6 | 7 | import { StyledCollectionsTree } from './collections-tree.styled'; 8 | import { CollectionNode } from './nodes'; 9 | 10 | const TreeWrapper = styled('div', { 11 | overflow: 'auto', 12 | }); 13 | 14 | export const CollectionsTree = (): JSX.Element => { 15 | const [filter, setFilter] = React.useState(''); 16 | 17 | const collections = useCollectionsStore((store) => store.filterCollections(filter)); 18 | 19 | const handleSearchInputChange = (event: React.ChangeEvent) => { 20 | const search = event.target.value.toLowerCase(); 21 | 22 | setFilter(search); 23 | }; 24 | 25 | return ( 26 | 27 | 40 | 41 | {collections.length ? ( 42 | > data={collections}> 43 | {collections.map((collection) => ( 44 | 45 | ))} 46 | 47 | ) : ( 48 |
56 | No collections 57 |
58 | )} 59 |
60 |
61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /src/app/pages/side-bar/nodes/method.node.tsx: -------------------------------------------------------------------------------- 1 | import { Spacer, Text, Tooltip } from '@nextui-org/react'; 2 | import React from 'react'; 3 | 4 | import { TreeNode, TreeNodeRendererProps } from '@components'; 5 | import { GrpcMethodType } from '@core/types'; 6 | import { CollectionType, GrpcMethod, useCollectionsStore, useTabsStore } from '@storage'; 7 | 8 | import { StreamBadge, UnaryBadge } from '../../collections/badge-types'; 9 | import { StyledNodeWrapper } from './node.styled'; 10 | 11 | export const GrpcMethodNode: React.FC> = ({ data }) => { 12 | const { createGrpcTab } = useTabsStore((store) => store); 13 | const collections = useCollectionsStore((store) => store.collections); 14 | 15 | const handleClick = () => { 16 | const nodeCollection = collections.find((collection) => 17 | collection.children?.find((service) => 18 | service.methods?.find((method) => method.id === data.id) 19 | ) 20 | ); 21 | 22 | const nodeService = nodeCollection?.children?.find((service) => 23 | service.methods?.find((method) => method.id === data.id) 24 | ); 25 | 26 | if (nodeCollection && nodeService) { 27 | createGrpcTab({ 28 | type: CollectionType.GRPC, 29 | title: data.name, 30 | info: { 31 | collectionId: nodeCollection.id, 32 | serviceId: nodeService.id, 33 | methodId: data.id, 34 | methodType: data.type, 35 | }, 36 | }); 37 | } 38 | }; 39 | 40 | const content = ( 41 | 42 | {data.type === GrpcMethodType.UNARY && } 43 | {data.type !== GrpcMethodType.UNARY && } 44 | 45 | 46 | {data.name} 47 | 48 | 49 | ); 50 | 51 | return ( 52 | 59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /src/app/pages/settings/forms/settings.form.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Dropdown } from '@nextui-org/react'; 2 | import React from 'react'; 3 | import { Controller, useForm } from 'react-hook-form'; 4 | 5 | import { Settings, ThemeType } from '@storage'; 6 | 7 | export interface SettingsFormProps { 8 | id?: string; 9 | 10 | defaultValues?: Partial; 11 | 12 | onSubmit: (payload: Settings) => void; 13 | } 14 | 15 | export const SettingsForm: React.FC = ({ 16 | onSubmit = () => {}, 17 | id, 18 | defaultValues, 19 | }) => { 20 | const { handleSubmit, control } = useForm({ defaultValues }); 21 | 22 | return ( 23 |
24 | 25 | ( 29 | 30 | 43 | {field.value} Theme 44 | 45 | { 52 | field.onChange(Array.from(keys).join('')); 53 | }} 54 | > 55 | Light 56 | Dark 57 | 58 | 59 | )} 60 | /> 61 | 62 |
63 | ); 64 | }; 65 | -------------------------------------------------------------------------------- /__tests__/tls-service/certs/gen-certs.sh: -------------------------------------------------------------------------------- 1 | rm *.pem 2 | rm *.srl 3 | rm *.cnf 4 | 5 | # 1. Generate CA's private key and self-signed certificate 6 | openssl req -x509 -newkey rsa:4096 -days 365 -nodes -keyout ca-key.pem -out ca-cert.pem -subj "/C=FR/ST=Occitanie/L=Toulouse/O=Test Org/OU=Test/CN=*.test/emailAddress=test@gmail.com" 7 | 8 | echo "CA's self-signed certificate" 9 | openssl x509 -in ca-cert.pem -noout -text 10 | 11 | # 2. Generate web server's private key and certificate signing request (CSR) 12 | openssl req -newkey rsa:4096 -nodes -keyout server-key.pem -out server-req.pem -subj "/C=FR/ST=Ile de France/L=Paris/O=Server TLS/OU=Server/CN=*.tls/emailAddress=tls@gmail.com" 13 | 14 | # Remember that when we develop on localhost, It’s important to add the IP:0.0.0.0 as an Subject Alternative Name (SAN) extension to the certificate. 15 | echo "subjectAltName=DNS:*.tls,DNS:localhost,IP:0.0.0.0" > server-ext.cnf 16 | # Or you can use localhost DNS and grpc.ssl_target_name_override variable 17 | # echo "subjectAltName=DNS:localhost" > server-ext.cnf 18 | 19 | # 3. Use CA's private key to sign web server's CSR and get back the signed certificate 20 | openssl x509 -req -in server-req.pem -days 60 -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -extfile server-ext.cnf 21 | 22 | echo "Server's signed certificate" 23 | openssl x509 -in server-cert.pem -noout -text 24 | 25 | # 4. Generate client's private key and certificate signing request (CSR) 26 | openssl req -newkey rsa:4096 -nodes -keyout client-key.pem -out client-req.pem -subj "/C=FR/ST=Alsace/L=Strasbourg/O=PC Client/OU=Computer/CN=*.client.com/emailAddress=client@gmail.com" 27 | 28 | # Remember that when we develop on localhost, It’s important to add the IP:0.0.0.0 as an Subject Alternative Name (SAN) extension to the certificate. 29 | echo "subjectAltName=DNS:*.client.com,IP:0.0.0.0" > client-ext.cnf 30 | 31 | # 5. Use CA's private key to sign client's CSR and get back the signed certificate 32 | openssl x509 -req -in client-req.pem -days 60 -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem -extfile client-ext.cnf 33 | 34 | echo "Client's signed certificate" 35 | openssl x509 -in client-cert.pem -noout -text 36 | -------------------------------------------------------------------------------- /src/app/pages/tabs-container/collection-types/grpc/response/unary-call.response.tsx: -------------------------------------------------------------------------------- 1 | import { Badge, Container, styled } from '@nextui-org/react'; 2 | import React from 'react'; 3 | 4 | import { CodeEditor, Tab, Tabs } from '@components'; 5 | import { GrpcMethodType, GrpcStatus } from '@core/types'; 6 | import { GrpcTab } from '@storage'; 7 | 8 | import { EmptyResponseView } from './empty-response-view'; 9 | 10 | const StyledContainer = styled('div', { 11 | display: 'flex', 12 | flex: 1, 13 | 14 | overflow: 'hidden', 15 | }); 16 | 17 | export interface UnaryCallResponseProps { 18 | tab: GrpcTab; 19 | } 20 | 21 | export const UnaryCallResponse: React.FC = ({ tab }) => { 22 | const responseStatus = ( 23 | 29 | {tab.data.response.code !== GrpcStatus.OK ? 'ERROR' : 'OK'} 30 | 31 | ); 32 | 33 | const responseTimings = ( 34 | 35 | {tab.data.response.timestamp || 0} ms 36 | 37 | ); 38 | 39 | const rightNode = tab.data.response.value && ( 40 | 46 | {responseStatus} 47 | {responseTimings} 48 | 49 | ); 50 | 51 | return ( 52 | 53 | 58 | 59 | {tab.data.response.value ? ( 60 | 67 | ) : ( 68 | 69 | )} 70 | 71 | 72 | 73 | ); 74 | }; 75 | -------------------------------------------------------------------------------- /src/app/pages/tabs-container/collection-types/grpc/send-header/protocol-switch.tsx: -------------------------------------------------------------------------------- 1 | import { faGlobe, faWarning } from '@fortawesome/free-solid-svg-icons'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { Container, Spacer, Switch, SwitchEvent, Text, Tooltip, useTheme } from '@nextui-org/react'; 4 | import React from 'react'; 5 | 6 | import { GrpcMethodType } from '@core/types'; 7 | import { GrpcProtocol, GrpcTab } from '@storage'; 8 | 9 | export interface ProtocolSwitchProps { 10 | tab: GrpcTab; 11 | 12 | onChange: (event: SwitchEvent) => void; 13 | } 14 | 15 | export const ProtocolSwitch: React.FC = ({ tab, onChange }) => { 16 | const { theme } = useTheme(); 17 | 18 | const isGrpcWebAvailable = 19 | tab.info.methodType === GrpcMethodType.UNARY || 20 | tab.info.methodType === GrpcMethodType.SERVER_STREAMING; 21 | 22 | let content = ( 23 | 24 | {tab.data.protocol === GrpcProtocol.GRPC_WEB ? 'Using gRPC-Web' : 'Using gRPC'} 25 | 26 | ); 27 | 28 | if (!isGrpcWebAvailable) { 29 | content = ( 30 | 31 | Using gRPC 32 | 33 | 34 | 35 | 36 | 37 | gRPC-Web doesn't support 38 | {tab.info.methodType === GrpcMethodType.CLIENT_STREAMING 39 | ? ' client streaming' 40 | : ' bidirectional streaming'} 41 | 42 | 43 | 44 | ); 45 | } 46 | 47 | return ( 48 | 49 | } 56 | checked={tab.data.protocol === GrpcProtocol.GRPC_WEB} 57 | onChange={onChange} 58 | /> 59 | 60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /src/app/hooks/protocols/grpc/prepare-request.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GrpcClientRequestOptions, 3 | GrpcMethodType, 4 | GrpcOptions, 5 | GrpcTlsConfig, 6 | GrpcTlsType, 7 | } from '@core/types'; 8 | import { Collection, CollectionType, GrpcTab, TlsPreset } from '@storage'; 9 | 10 | function getRequestAddress(tab: GrpcTab): string { 11 | if (tab.data.url && tab.data.url.length > 0) { 12 | return tab.data.url; 13 | } 14 | 15 | throw new Error('Address is empty.'); 16 | } 17 | 18 | export function getTlsOptions(presets: TlsPreset[], id?: string): GrpcTlsConfig { 19 | const preset = presets.find((item) => item.id === id); 20 | 21 | if (preset) { 22 | return preset.tls; 23 | } 24 | 25 | return { 26 | type: GrpcTlsType.INSECURE, 27 | }; 28 | } 29 | 30 | export function getOptions( 31 | collections: Collection[], 32 | tab: GrpcTab, 33 | tls: GrpcTlsConfig 34 | ): [GrpcOptions, GrpcClientRequestOptions] { 35 | const collection = collections.find((item) => item.id === tab.info.collectionId); 36 | const service = collection?.children?.find((item) => item.id === tab.info.serviceId); 37 | const method = service?.methods?.find((item) => item.id === tab.info.methodId); 38 | 39 | if (collection && service && method) { 40 | return [ 41 | collection.options, 42 | { 43 | serviceName: service.name, 44 | methodName: method.name, 45 | address: getRequestAddress(tab), 46 | tls, 47 | }, 48 | ]; 49 | } 50 | 51 | throw new Error(`Couldn't get request options. Try to sync collection.`); 52 | } 53 | 54 | export function parseRequest(tab: GrpcTab): Record { 55 | try { 56 | const request = JSON.parse(tab.data.requestTabs.request.value?.trim() || '{}'); 57 | 58 | return request; 59 | } catch (error) { 60 | throw new Error(`Couldn't parse request message.`); 61 | } 62 | } 63 | 64 | export function parseMetadata(tab: GrpcTab): Record { 65 | try { 66 | const metadata = JSON.parse(tab.data.requestTabs.metadata.value?.trim() || '{}'); 67 | 68 | return metadata; 69 | } catch (error) { 70 | throw new Error(`Couldn't parse request metadata.`); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/app/pages/tabs-container/collection-types/grpc/send-header/send-header.server-streaming.tsx: -------------------------------------------------------------------------------- 1 | import { faXmark } from '@fortawesome/free-solid-svg-icons'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { Button, Loading, Spacer } from '@nextui-org/react'; 4 | import React from 'react'; 5 | import { useUnmount } from 'react-use'; 6 | 7 | import { GrpcMethodType } from '@core/types'; 8 | import { useGrpcTabContextStore, useServerStreaming } from '@hooks'; 9 | 10 | import { SendHeader, SendHeaderProps } from './send-header.basic'; 11 | 12 | export const ServerStreamingSendHeader: React.FC< 13 | SendHeaderProps 14 | > = ({ tab }) => { 15 | const { invoke, cancel } = useServerStreaming(); 16 | const { getContext } = useGrpcTabContextStore(); 17 | 18 | const context = getContext(tab.id); 19 | 20 | const handleInvokeButtonClick = async () => { 21 | await invoke(tab); 22 | }; 23 | 24 | const handleCancelButtonClick = async () => { 25 | await cancel(tab); 26 | }; 27 | 28 | useUnmount(() => { 29 | handleCancelButtonClick(); 30 | }); 31 | 32 | return ( 33 | 34 | {!!context?.isServerStreaming && ( 35 | <> 36 | 37 | 69 | 70 | ); 71 | }; 72 | -------------------------------------------------------------------------------- /src/core/clients/grpc/interfaces/client.interface.ts: -------------------------------------------------------------------------------- 1 | export enum GrpcTlsType { 2 | INSECURE = 'insecure', 3 | SERVER_SIDE = 'server-side', 4 | MUTUAL = 'mutual', 5 | } 6 | 7 | export type GrpcTlsConfig = T extends GrpcTlsType.MUTUAL 8 | ? GrpcMutualTlsConfig 9 | : T extends GrpcTlsType.SERVER_SIDE 10 | ? GrpcServerSideTlsConfig 11 | : GrpcInsecureTlsConfig; 12 | 13 | /** 14 | * https://grpc.github.io/grpc/core/group__grpc__arg__keys.html 15 | */ 16 | export interface GrpcChannelOptions { 17 | /** 18 | * This should be used for testing only. 19 | * 20 | * The caller of the secure_channel_create functions may override 21 | * the target name used for SSL host name checking using this channel 22 | * argument which is of type GRPC_ARG_STRING. If this argument is 23 | * not specified, the name used for SSL host name checking will be 24 | * the target parameter (assuming that the secure channel is an SSL channel). 25 | * If this parameter is specified and the underlying is not an SSL channel, it will just be ignored. 26 | */ 27 | sslTargetNameOverride?: string; 28 | } 29 | 30 | export interface GrpcServerSideTlsConfig { 31 | type: GrpcTlsType.SERVER_SIDE; 32 | 33 | rootCertificatePath?: string; 34 | 35 | channelOptions?: GrpcChannelOptions; 36 | } 37 | 38 | export interface GrpcMutualTlsConfig { 39 | type: GrpcTlsType.MUTUAL; 40 | 41 | rootCertificatePath?: string; 42 | 43 | clientCertificatePath: string; 44 | 45 | clientKeyPath: string; 46 | 47 | channelOptions?: GrpcChannelOptions; 48 | } 49 | 50 | export interface GrpcInsecureTlsConfig { 51 | type: GrpcTlsType.INSECURE; 52 | 53 | channelOptions?: GrpcChannelOptions; 54 | } 55 | 56 | export interface GrpcClientRequestOptions { 57 | serviceName: string; 58 | 59 | methodName: string; 60 | 61 | address: string; 62 | 63 | tls: GrpcTlsConfig; 64 | } 65 | 66 | export function isInsecureTlsConfig( 67 | config: GrpcTlsConfig 68 | ): config is GrpcTlsConfig { 69 | return config.type === GrpcTlsType.INSECURE; 70 | } 71 | 72 | export function isMutualTlsConfig( 73 | config: GrpcTlsConfig 74 | ): config is GrpcTlsConfig { 75 | return config.type === GrpcTlsType.MUTUAL; 76 | } 77 | -------------------------------------------------------------------------------- /src/main/clients/grpc-client/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents channel from renderer process 3 | */ 4 | export enum GrpcClientChannel { 5 | INVOKE_UNARY_REQUEST = 'grpc-client-channel:unary-request:invoke', 6 | 7 | INVOKE_SERVER_STREAMING_REQUEST = 'grpc-client-channel:server-streaming-request:invoke', 8 | CANCEL_SERVER_STREAMING_REQUEST = 'grpc-client-channel:server-streaming-request:cancel', 9 | 10 | INVOKE_CLIENT_STREAMING_REQUEST = 'grpc-client-channel:client-streaming-request:invoke', 11 | SEND_CLIENT_STREAMING_REQUEST = 'grpc-client-channel:client-streaming-request:send', 12 | END_CLIENT_STREAMING_REQUEST = 'grpc-client-channel:client-streaming-request:end', 13 | CANCEL_CLIENT_STREAMING_REQUEST = 'grpc-client-channel:client-streaming-request:cancel', 14 | 15 | INVOKE_BIDIRECTIONAL_STREAMING_REQUEST = 'grpc-client-channel:bidirectional-streaming-request:invoke', 16 | SEND_BIDIRECTIONAL_STREAMING_REQUEST = 'grpc-client-channel:bidirectional-streaming-request:send', 17 | END_BIDIRECTIONAL_STREAMING_REQUEST = 'grpc-client-channel:bidirectional-streaming-request:end', 18 | CANCEL_BIDIRECTIONAL_STREAMING_REQUEST = 'grpc-client-channel:bidirectional-streaming-request:cancel', 19 | } 20 | 21 | /** 22 | * Represents channel from main process 23 | */ 24 | export enum GrpcClientServerStreamingChannel { 25 | DATA = 'grpc-client:server-streaming-request:data', 26 | ERROR = 'grpc-client:server-streaming-request:error', 27 | END = 'grpc-client:server-streaming-request:end', 28 | CANCEL = 'grpc-client:server-streaming-request:cancel', 29 | } 30 | 31 | /** 32 | * Represents channel from main process 33 | */ 34 | export enum GrpcClientClientStreamingChannel { 35 | DATA = 'grpc-client:client-streaming-request:data', 36 | ERROR = 'grpc-client:client-streaming-request:error', 37 | END = 'grpc-client:client-streaming-request:end', 38 | CANCEL = 'grpc-client:client-streaming-request:cancel', 39 | } 40 | 41 | /** 42 | * Represents channel from main process 43 | */ 44 | export enum GrpcClientBidirectionalStreamingChannel { 45 | DATA = 'grpc-client:bidirectional-streaming-request:data', 46 | ERROR = 'grpc-client:bidirectional-streaming-request:error', 47 | END = 'grpc-client:bidirectional-streaming-request:end', 48 | CANCEL = 'grpc-client:bidirectional-streaming-request:cancel', 49 | } 50 | -------------------------------------------------------------------------------- /src/app/pages/tabs-container/collection-types/grpc/environments/environment.form.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Input, Spacer } from '@nextui-org/react'; 2 | import chroma from 'chroma-js'; 3 | import React from 'react'; 4 | import { useForm } from 'react-hook-form'; 5 | 6 | import { ColorPickerInput } from '@components'; 7 | import { Environment } from '@storage'; 8 | 9 | export interface EnvironmentFormProps { 10 | id?: string; 11 | 12 | defaultValues?: Partial>; 13 | 14 | onSubmit: (payload: Environment) => void; 15 | } 16 | 17 | export const EnvironmentForm: React.FC = ({ 18 | onSubmit = () => {}, 19 | id, 20 | defaultValues, 21 | }) => { 22 | const { 23 | register, 24 | handleSubmit, 25 | formState: { errors }, 26 | setValue, 27 | watch, 28 | } = useForm({ defaultValues: { color: chroma.random().hex(), ...defaultValues } }); 29 | 30 | return ( 31 |
32 | 33 | 42 | 54 | 55 | setValue('color', newColor)} 58 | /> 59 | 60 | 61 | 71 | 72 |
73 | ); 74 | }; 75 | -------------------------------------------------------------------------------- /src/app/hooks/protocols/grpc/use-unary-call.ts: -------------------------------------------------------------------------------- 1 | import { notification } from '@components'; 2 | import { GrpcMethodType } from '@core/types'; 3 | import { 4 | GrpcProtocol, 5 | GrpcTab, 6 | useCollectionsStore, 7 | useTabsStore, 8 | useTlsPresetsStore, 9 | } from '@storage'; 10 | 11 | import { getOptions, getTlsOptions, parseMetadata, parseRequest } from './prepare-request'; 12 | import { useGrpcTabContextStore } from './use-grpc-tab-context'; 13 | 14 | export function useUnaryCall() { 15 | const collections = useCollectionsStore((store) => store.collections); 16 | const { updateGrpcTabData } = useTabsStore((store) => store); 17 | const tlsPresets = useTlsPresetsStore((store) => store.presets); 18 | const { setContext, getContext, deleteContext } = useGrpcTabContextStore(); 19 | 20 | function getClient(tab: GrpcTab) { 21 | return tab.data.protocol === GrpcProtocol.GRPC ? window.clients.grpc : window.clients.grpcWeb; 22 | } 23 | 24 | function isRequestLoading(tab: GrpcTab) { 25 | if (getContext(tab.id)?.isLoading) { 26 | throw new Error('Request already invoked'); 27 | } 28 | } 29 | 30 | async function invoke(tab: GrpcTab): Promise { 31 | try { 32 | isRequestLoading(tab); 33 | 34 | setContext(tab.id, { isLoading: true }); 35 | const tls = getTlsOptions(tlsPresets, tab.data.tlsId); 36 | const [grpcOptions, requestOptions] = getOptions(collections, tab, tls); 37 | const request = parseRequest(tab); 38 | const metadata = parseMetadata(tab); 39 | 40 | const client = getClient(tab); 41 | 42 | const response = await client.unary.invoke(grpcOptions, requestOptions, request, metadata); 43 | 44 | updateGrpcTabData(tab.id, { 45 | response: { 46 | ...tab.data.response, 47 | code: response.code, 48 | timestamp: response.timestamp, 49 | value: JSON.stringify(response.value, null, 2), 50 | }, 51 | }); 52 | } catch (error: any) { 53 | notification( 54 | { title: 'Invoke request error', description: error.message }, 55 | { type: 'error', position: 'bottom-right' } 56 | ); 57 | } finally { 58 | deleteContext(tab.id); 59 | } 60 | } 61 | 62 | return { invoke }; 63 | } 64 | -------------------------------------------------------------------------------- /src/app/storage/interfaces/tabs/tabs.interface.ts: -------------------------------------------------------------------------------- 1 | import { GrpcMethodType } from '@core/types'; 2 | 3 | import { CollectionType } from '../collections.interface'; 4 | import { GrpcStreamMessage, GrpcTabData, GrpcTabInfo } from './grpc-tab.interface'; 5 | 6 | export type Tab = { 7 | id: string; 8 | title: string; 9 | type: T; 10 | 11 | info: TabInfo; 12 | data: TabData; 13 | }; 14 | 15 | export type GrpcTab = Tab< 16 | CollectionType.GRPC, 17 | GrpcTabInfo, 18 | GrpcTabData 19 | >; 20 | 21 | export function isGrpcTab(tab: Tab): tab is Tab { 22 | return tab.type === CollectionType.GRPC; 23 | } 24 | 25 | export function isGrpcTabUnaryCall( 26 | tab: GrpcTab 27 | ): tab is GrpcTab { 28 | return tab.info.methodType === GrpcMethodType.UNARY; 29 | } 30 | 31 | export function isGrpcTabServerStreaming( 32 | tab: GrpcTab 33 | ): tab is GrpcTab { 34 | return tab.info.methodType === GrpcMethodType.SERVER_STREAMING; 35 | } 36 | 37 | export function isGrpcTabClientStreaming( 38 | tab: GrpcTab 39 | ): tab is GrpcTab { 40 | return tab.info.methodType === GrpcMethodType.CLIENT_STREAMING; 41 | } 42 | 43 | export function isGrpcTabBidirectionalStreaming( 44 | tab: GrpcTab 45 | ): tab is GrpcTab { 46 | return tab.info.methodType === GrpcMethodType.BIDIRECTIONAL_STREAMING; 47 | } 48 | 49 | export interface TabsStorage { 50 | tabs: Tab[]; 51 | 52 | activeTabId: string | undefined; 53 | 54 | closeAllTabs: () => void; 55 | closeActiveTab: () => void; 56 | closeTab: (id: string) => void; 57 | activateTab: (id: string) => void; 58 | moveTab: (currentId: string, overId: string | undefined) => void; 59 | 60 | createGrpcTab: (payload: Pick, 'title' | 'type' | 'info'>) => void; 61 | addGrpcStreamMessage: ( 62 | id: string, 63 | message: Omit, 64 | forceClear?: boolean 65 | ) => void; 66 | updateGrpcTabData: (id: string, data: Partial>) => void; 67 | updateGrpcTabsEnvironment: (currentEnvironmentId: string, newEnvironmentId?: string) => void; 68 | } 69 | -------------------------------------------------------------------------------- /src/app/pages/logs/logs.modal.tsx: -------------------------------------------------------------------------------- 1 | import { faBroomBall } from '@fortawesome/free-solid-svg-icons'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { Button, Modal, ModalProps, styled, Text } from '@nextui-org/react'; 4 | import { nanoid } from 'nanoid'; 5 | import React from 'react'; 6 | 7 | import { useLogsStore } from '@storage'; 8 | 9 | const LogItem = styled('div', { 10 | paddingLeft: 10, 11 | paddingRight: 10, 12 | '&:hover': { 13 | backgroundColor: '$backgroundContrast', 14 | }, 15 | }); 16 | 17 | export const LogsModal: React.FC = ({ onClose = () => {}, ...props }) => { 18 | const { logs, clearLogs } = useLogsStore((store) => store); 19 | 20 | const content = 21 | logs.length > 0 ? ( 22 | logs.map((log) => ( 23 | 24 | 25 | {log.message} 26 | 27 | 28 | )) 29 | ) : ( 30 |
38 | No logs 39 |
40 | ); 41 | 42 | const handleClearButtonClick = () => { 43 | clearLogs(); 44 | }; 45 | 46 | return ( 47 | 53 | 59 | Logs 60 | 61 | 62 | {content} 63 | 64 | 65 | 76 | 79 | 80 | 81 | ); 82 | }; 83 | -------------------------------------------------------------------------------- /src/app/pages/collections/modals/create-collection.modal.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Modal, ModalProps, Spacer, Switch, Text } from '@nextui-org/react'; 2 | import React from 'react'; 3 | 4 | import { Badge } from '@components'; 5 | import { useCreateCollection } from '@hooks'; 6 | import { Collection, CollectionType } from '@storage'; 7 | 8 | import { CollectionForm } from '../forms'; 9 | 10 | export const CreateCollectionModal: React.FC = ({ onClose = () => {}, ...props }) => { 11 | const { create } = useCreateCollection(); 12 | const [isCreateMore, setIsCreateMore] = React.useState(false); 13 | const [defaultValues, setDefaultValues] = React.useState({}); 14 | 15 | const handleSubmit = async (payload: Collection) => { 16 | await create({ 17 | ...payload, 18 | type: CollectionType.GRPC, 19 | }); 20 | 21 | if (isCreateMore) { 22 | setDefaultValues({ 23 | type: CollectionType.GRPC, 24 | options: { 25 | includeDirs: payload.options.includeDirs, 26 | }, 27 | }); 28 | } else { 29 | onClose(); 30 | } 31 | }; 32 | 33 | return ( 34 | 41 | 42 | 43 | New Collection 44 | 45 | 46 | 47 | 48 | 53 | 54 | 55 | setIsCreateMore(event.target.checked)} 59 | /> 60 | 61 | Create more 62 | 63 | 73 | 74 | 75 | ); 76 | }; 77 | -------------------------------------------------------------------------------- /src/app/components/code-editor/code-editor.tsx: -------------------------------------------------------------------------------- 1 | import { defaultKeymap, indentWithTab } from '@codemirror/commands'; 2 | import { json } from '@codemirror/lang-json'; 3 | // import { json, jsonParseLinter } from '@codemirror/lang-json'; 4 | // import { linter, lintGutter } from '@codemirror/lint'; 5 | import { keymap, ViewUpdate } from '@codemirror/view'; 6 | import { useTheme } from '@nextui-org/react'; 7 | import { BasicSetupOptions } from '@uiw/react-codemirror'; 8 | import React from 'react'; 9 | 10 | import { StyledCodeMirror } from './code-editor.styled'; 11 | import { createTheme } from './themes/theme'; 12 | 13 | export interface CodeEditorProps { 14 | value?: string; 15 | 16 | height?: string; 17 | 18 | width?: string; 19 | 20 | maxHeight?: string; 21 | 22 | maxWidth?: string; 23 | 24 | readOnly?: boolean; 25 | 26 | basicSetup?: BasicSetupOptions; 27 | 28 | onChange?(value: string, viewUpdate?: ViewUpdate): void; 29 | } 30 | 31 | export const CodeEditor: React.FC = ({ 32 | maxHeight, 33 | maxWidth, 34 | height = 'auto', 35 | width = 'auto', 36 | value, 37 | readOnly = false, 38 | basicSetup, 39 | onChange = () => {}, 40 | }) => { 41 | const { theme, isDark } = useTheme(); 42 | 43 | return ( 44 | shortcut.key !== 'Mod-Enter'), 73 | ]), 74 | json(), 75 | ]} 76 | // extensions={[keymap.of([indentWithTab]), json()]} 77 | // extensions={[keymap.of([indentWithTab]), json(), linter(jsonParseLinter()), lintGutter()]} 78 | /> 79 | ); 80 | }; 81 | -------------------------------------------------------------------------------- /src/app/components/notification/notification.tsx: -------------------------------------------------------------------------------- 1 | import { faClone, faXmark } from '@fortawesome/free-solid-svg-icons'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { Button, Spacer, styled, Text } from '@nextui-org/react'; 4 | import React from 'react'; 5 | import { ToastContentProps } from 'react-toastify'; 6 | import { useCopyToClipboard } from 'react-use'; 7 | 8 | const NotificationWrapper = styled('div', { 9 | display: 'flex', 10 | flexDirection: 'column', 11 | 12 | maxWidth: '100%', 13 | 14 | userSelect: 'none', 15 | overflowWrap: 'anywhere', 16 | }); 17 | 18 | const TitleWrapper = styled('div', { 19 | display: 'flex', 20 | flexWrap: 'nowrap', 21 | }); 22 | 23 | const CommandsWrapper = styled('div', { 24 | display: 'flex', 25 | marginLeft: 'auto', 26 | }); 27 | 28 | export type NotificationProps = { 29 | title: string; 30 | description?: string; 31 | } & Partial; 32 | 33 | export const Notification: React.FC = ({ title, description, closeToast }) => { 34 | const [, copyToClipboard] = useCopyToClipboard(); 35 | const handleCopyButtonClick = () => copyToClipboard(description || title || ''); 36 | 37 | return ( 38 | 39 | 40 | {title} 41 | 42 |