├── 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 | }
28 | onClick={() => onClick(!isOpen)}
29 | />
30 | );
31 |
--------------------------------------------------------------------------------
/src/app/components/tabs/hooks/use-on-screen.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export function useOnScreen(ref: React.RefObject, rootMargin = '0px') {
4 | // State and setter for storing whether element is visible
5 | const [isIntersecting, setIntersecting] = React.useState(false);
6 |
7 | React.useEffect(() => {
8 | const observer = new IntersectionObserver(
9 | ([entry]) => {
10 | // Update our state when observer callback fires
11 | setIntersecting(entry.isIntersecting);
12 | },
13 | {
14 | rootMargin,
15 | }
16 | );
17 |
18 | if (ref.current) {
19 | observer.observe(ref.current);
20 | }
21 |
22 | return () => {
23 | if (ref.current) {
24 | observer.unobserve(ref.current);
25 | }
26 | };
27 | }, []); // Empty array ensures that effect is only run on mount and unmount
28 |
29 | return isIntersecting;
30 | }
31 |
--------------------------------------------------------------------------------
/src/app/components/color-picker/color-picker.tsx:
--------------------------------------------------------------------------------
1 | import { Popover, styled } from '@nextui-org/react';
2 | import React from 'react';
3 | import { ChromePicker, ColorChangeHandler } from 'react-color';
4 |
5 | const StyledChromePicker = styled(ChromePicker);
6 |
7 | export interface ColorPickerProps {
8 | trigger: React.ReactNode;
9 |
10 | color: string;
11 |
12 | isOpen: boolean;
13 |
14 | onOpenChange: (isOpen: boolean) => void;
15 | onColorChange: ColorChangeHandler;
16 | }
17 |
18 | export const ColorPicker: React.FC = ({
19 | isOpen,
20 | trigger,
21 | color,
22 | onColorChange,
23 | onOpenChange,
24 | }) => (
25 |
26 | {trigger}
27 |
28 |
29 |
30 |
31 | );
32 |
--------------------------------------------------------------------------------
/__tests__/tls-service/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tls-service",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "scripts": {
6 | "start:mutual": "TLS_MODE=mutual GRPC_TRACE=all GRPC_VERBOSITY=DEBUG ts-node ./src/index.ts",
7 | "start:server": "TLS_MODE=server GRPC_TRACE=all GRPC_VERBOSITY=DEBUG ts-node ./src/index.ts",
8 | "client:server": "TLS_MODE=server ts-node ./src/client.ts",
9 | "client:mutual": "TLS_MODE=mutual ts-node ./src/client.ts",
10 | "certs": "cd certs && ./gen-certs.sh",
11 | "proto": "protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_opt=env=node,outputServices=grpc-js --ts_proto_out=./src/generated ./proto/tls_service.proto",
12 | "build": "npm run proto && npm run certs"
13 | },
14 | "dependencies": {
15 | "@grpc/grpc-js": "1.7.3"
16 | },
17 | "devDependencies": {
18 | "ts-node": "10.9.1",
19 | "ts-proto": "1.131.0",
20 | "typescript": "4.8.4"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/clients/grpc-client/preload/unary.ts:
--------------------------------------------------------------------------------
1 | import { ipcRenderer } from 'electron';
2 |
3 | import { GrpcClientRequestOptions, GrpcOptions, GrpcResponse } from '@core';
4 |
5 | import { parseErrorFromIPCMain } from '../../../common';
6 | import { GrpcClientChannel } from '../constants';
7 |
8 | export default {
9 | async invoke = Record>(
10 | options: GrpcOptions,
11 | requestOptions: GrpcClientRequestOptions,
12 | payload: Record,
13 | metadata?: Record
14 | ): Promise> {
15 | try {
16 | const response = await ipcRenderer.invoke(
17 | GrpcClientChannel.INVOKE_UNARY_REQUEST,
18 | options,
19 | requestOptions,
20 | payload,
21 | metadata
22 | );
23 |
24 | return response;
25 | } catch (error) {
26 | throw new Error(parseErrorFromIPCMain(error));
27 | }
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/src/app/components/code-editor/themes/theme.ts:
--------------------------------------------------------------------------------
1 | import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
2 | import { Extension } from '@codemirror/state';
3 | import { EditorView } from '@codemirror/view';
4 | import { tags as t } from '@lezer/highlight';
5 |
6 | export interface ThemeColors {
7 | property: string;
8 | number: string;
9 | boolean: string;
10 | string: string;
11 | null: string;
12 | }
13 |
14 | export function createTheme(colors: ThemeColors, isDark: boolean = false): Extension {
15 | const theme = EditorView.theme({}, { dark: isDark });
16 |
17 | const highlightStyle = HighlightStyle.define([
18 | { tag: t.propertyName, color: colors.property },
19 | { tag: t.number, color: colors.number },
20 | { tag: t.bool, color: colors.boolean },
21 | { tag: t.string, color: colors.string },
22 | { tag: t.null, color: colors.null },
23 | ]);
24 |
25 | return [theme, syntaxHighlighting(highlightStyle)];
26 | }
27 |
--------------------------------------------------------------------------------
/src/app/components/icons/ezy.icon.tsx:
--------------------------------------------------------------------------------
1 | import { CSS, styled } from '@nextui-org/react';
2 | import React from 'react';
3 |
4 | export interface Props {
5 | className?: string;
6 | css?: CSS;
7 | }
8 |
9 | const StyledSvg = styled('svg', {
10 | stroke: 'CurrentColor',
11 | });
12 |
13 | export const EzyIcon: React.FC = () => (
14 |
21 |
26 |
27 | );
28 |
--------------------------------------------------------------------------------
/src/main/clients/grpc-web-client/preload/unary.ts:
--------------------------------------------------------------------------------
1 | import { ipcRenderer } from 'electron';
2 |
3 | import { GrpcClientRequestOptions, GrpcOptions, GrpcResponse } from '@core';
4 |
5 | import { parseErrorFromIPCMain } from '../../../common';
6 | import { GrpcWebClientChannel } from '../constants';
7 |
8 | export default {
9 | async invoke = Record>(
10 | options: GrpcOptions,
11 | requestOptions: GrpcClientRequestOptions,
12 | payload: Record,
13 | metadata?: Record
14 | ): Promise> {
15 | try {
16 | const response = await ipcRenderer.invoke(
17 | GrpcWebClientChannel.INVOKE_UNARY_REQUEST,
18 | options,
19 | requestOptions,
20 | payload,
21 | metadata
22 | );
23 |
24 | return response;
25 | } catch (error) {
26 | throw new Error(parseErrorFromIPCMain(error));
27 | }
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | *.log
3 | npm-debug.log*
4 | yarn-debug.log*
5 | yarn-error.log*
6 | lerna-debug.log*
7 |
8 | # Diagnostic reports (https://nodejs.org/api/report.html)
9 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
10 |
11 | # Runtime data
12 | pids
13 | *.pid
14 | *.seed
15 | *.pid.lock
16 | .DS_Store
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Compiled binary addons (https://nodejs.org/api/addons.html)
29 | build/Release
30 |
31 | # Dependency directories
32 | node_modules/
33 |
34 | # TypeScript cache
35 | *.tsbuildinfo
36 |
37 | # Optional npm cache directory
38 | .npm
39 |
40 | # Optional eslint cache
41 | .eslintcache
42 |
43 | # Optional REPL history
44 | .node_repl_history
45 |
46 | # Output of 'npm pack'
47 | *.tgz
48 |
49 | # Webpack
50 | .webpack/
51 |
52 | # Electron-Forge
53 | out/
54 |
--------------------------------------------------------------------------------
/src/app/storage/environments.storage.ts:
--------------------------------------------------------------------------------
1 | import { produce } from 'immer';
2 | import create from 'zustand';
3 | import { persist } from 'zustand/middleware';
4 |
5 | import { EnvironmentsStorage } from './interfaces';
6 |
7 | export const useEnvironmentsStore = create(
8 | persist(
9 | (set) => ({
10 | environments: [],
11 | createEnvironment: (environment) =>
12 | set(
13 | produce((state) => {
14 | state.environments.push(environment);
15 | })
16 | ),
17 | removeEnvironment: (id) =>
18 | set(
19 | produce((state) => {
20 | const index = state.environments.findIndex((environment) => environment.id === id);
21 | if (index !== -1) state.environments.splice(index, 1);
22 | })
23 | ),
24 | }),
25 | {
26 | name: 'environments',
27 | getStorage: () => window.electronStore,
28 | }
29 | )
30 | );
31 |
--------------------------------------------------------------------------------
/src/app/hooks/collections/use-create-collection.ts:
--------------------------------------------------------------------------------
1 | import { notification } from '@components';
2 | import { Collection, CollectionType, useCollectionsStore } from '@storage';
3 |
4 | export function useCreateCollection() {
5 | const createCollection = useCollectionsStore((store) => store.createCollection);
6 |
7 | const create = async (payload: Collection) => {
8 | try {
9 | await createCollection({
10 | ...payload,
11 | type: CollectionType.GRPC,
12 | });
13 |
14 | notification(
15 | {
16 | title: `${payload.name}`,
17 | description: 'Collection successfully created',
18 | },
19 | { type: 'success', position: 'top-right' }
20 | );
21 | } catch (error: any) {
22 | notification(
23 | {
24 | title: `Create collection error`,
25 | description: error?.message,
26 | },
27 | { type: 'error', position: 'top-right' }
28 | );
29 | }
30 | };
31 |
32 | return { create };
33 | }
34 |
--------------------------------------------------------------------------------
/src/app/pages/shortcuts/hooks/use-theme-actions.tsx:
--------------------------------------------------------------------------------
1 | import { faPalette } from '@fortawesome/free-solid-svg-icons';
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3 | import { useRegisterActions } from '@getezy/kbar';
4 | import React from 'react';
5 |
6 | import { ThemeType, useSettingsStore } from '@storage';
7 |
8 | export function useThemeActions() {
9 | const { updateTheme } = useSettingsStore((store) => store);
10 |
11 | useRegisterActions([
12 | {
13 | id: 'theme',
14 | section: 'Settings',
15 | name: 'Change Theme',
16 | icon: ,
17 | },
18 | {
19 | id: 'darkTheme',
20 | name: 'Dark',
21 | keywords: 'dark theme',
22 | parent: 'theme',
23 | perform: () => updateTheme(ThemeType.DARK),
24 | },
25 | {
26 | id: 'lightTheme',
27 | name: 'Light',
28 | keywords: 'light theme',
29 | parent: 'theme',
30 | perform: () => updateTheme(ThemeType.LIGHT),
31 | },
32 | ]);
33 | }
34 |
--------------------------------------------------------------------------------
/webpack.renderer.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const rules = require('./webpack.rules');
4 | const plugins = require('./webpack.plugins');
5 |
6 | rules.push({
7 | test: /\.css$/,
8 | use: [{ loader: 'style-loader' }, { loader: 'css-loader', options: { import: true } }],
9 | });
10 |
11 | module.exports = {
12 | module: {
13 | rules,
14 | },
15 | plugins: plugins,
16 | resolve: {
17 | extensions: ['.js', '.ts', '.jsx', '.tsx', '.css'],
18 | alias: {
19 | '@components': path.resolve(__dirname, 'src/app/components/index.ts'),
20 | '@hooks': path.resolve(__dirname, 'src/app/hooks/index.ts'),
21 | '@storage': path.resolve(__dirname, 'src/app/storage/index.ts'),
22 | '@context': path.resolve(__dirname, './src/app/context/index.ts'),
23 | '@layouts': path.resolve(__dirname, './src/app/layouts/index.ts'),
24 | '@core$': path.resolve(__dirname, './src/core/index.ts'),
25 | '@core/types': path.resolve(__dirname, './src/core/typings.ts'),
26 | },
27 | },
28 | };
29 |
--------------------------------------------------------------------------------
/src/app/pages/collections/badge-types/grpc/stream.badge.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | faArrowLeft,
3 | faArrowRight,
4 | faArrowRightArrowLeft,
5 | } from '@fortawesome/free-solid-svg-icons';
6 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
7 | import React from 'react';
8 |
9 | import { Badge } from '@components';
10 | import { GrpcMethodType } from '@core/types';
11 |
12 | export type StreamBadeProps = {
13 | type:
14 | | GrpcMethodType.CLIENT_STREAMING
15 | | GrpcMethodType.SERVER_STREAMING
16 | | GrpcMethodType.BIDIRECTIONAL_STREAMING;
17 | };
18 |
19 | const StreamBadgeIcons = {
20 | [GrpcMethodType.CLIENT_STREAMING]: faArrowRight,
21 | [GrpcMethodType.SERVER_STREAMING]: faArrowLeft,
22 | [GrpcMethodType.BIDIRECTIONAL_STREAMING]: faArrowRightArrowLeft,
23 | };
24 |
25 | export const StreamBadge: React.FC = ({ type }) => {
26 | const icon = ;
27 |
28 | return ;
29 | };
30 |
--------------------------------------------------------------------------------
/src/app/pages/tabs-container/tabs-container.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Tab, Tabs } from '@components';
4 | import { CollectionType, useTabsStore } from '@storage';
5 |
6 | import { GrpcTabContainer } from './collection-types';
7 | import { WelcomeContainer } from './welcome';
8 |
9 | export const TabsContainer = (): JSX.Element => {
10 | const { activeTabId, closeTab, activateTab, moveTab, tabs } = useTabsStore((store) => store);
11 |
12 | const tabsContent = tabs.map((tab) => (
13 |
14 | {tab.type === CollectionType.GRPC && }
15 |
16 | ));
17 |
18 | return tabs.length ? (
19 |
27 | {tabsContent}
28 |
29 | ) : (
30 |
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/__tests__/simple-service/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 | string snake_case_field = 2;
10 | string camelCaseField = 3;
11 | }
12 |
13 | message LongIntegersMessage {
14 | int64 int = 1;
15 | uint64 uint = 2;
16 | sint64 sint = 3;
17 | fixed64 fint = 4;
18 | sfixed64 sfint = 5;
19 | }
20 |
21 | service SimpleService {
22 | rpc Unary(SimpleMessage) returns (SimpleMessage);
23 | rpc UnaryWithError(SimpleMessage) returns (SimpleMessage);
24 |
25 | rpc LongIntegers(LongIntegersMessage) returns (LongIntegersMessage);
26 |
27 | rpc ClientStreamingRequest(stream SimpleMessage) returns (SimpleMessage);
28 | rpc ClientStreamingRequestWithError(stream SimpleMessage)
29 | returns (SimpleMessage);
30 |
31 | rpc ServerStreamingRequest(google.protobuf.Empty)
32 | returns (stream SimpleMessage);
33 |
34 | rpc BidirectionalStreamingRequest(stream SimpleMessage)
35 | returns (stream SimpleMessage);
36 | }
37 |
--------------------------------------------------------------------------------
/src/app/components/tabs/tab-bar/tab-bar-item/tab-bar-item-draggable.tsx:
--------------------------------------------------------------------------------
1 | import { useSortable } from '@dnd-kit/sortable';
2 | import React, { PropsWithChildren } from 'react';
3 |
4 | import { StyledTabBarItem, TabBarItemProps } from './tab-bar-item.styled';
5 |
6 | export const TabBarItemDraggable = React.forwardRef<
7 | HTMLDivElement,
8 | PropsWithChildren
9 | >(({ children, active = false, closable = false, onClick, id }, ref) => {
10 | const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
11 | id,
12 | });
13 |
14 | const style: React.CSSProperties = {
15 | visibility: isDragging ? 'hidden' : 'visible',
16 | transform: transform ? `translate3d(${transform.x}px, 0px, 0)` : undefined,
17 | transition,
18 | };
19 |
20 | return (
21 |
22 |
23 | {children}
24 |
25 |
26 | );
27 | });
28 |
--------------------------------------------------------------------------------
/src/app/components/tabs/tab-bar/tab-bar-item/tab-bar-item.styled.ts:
--------------------------------------------------------------------------------
1 | import { styled, VariantProps } from '@nextui-org/react';
2 |
3 | export const StyledTabBarItem = styled('div', {
4 | display: 'flex',
5 | flexWrap: 'nowrap',
6 | whiteSpace: 'nowrap',
7 | alignItems: 'baseline',
8 | fontFamily: '$mono',
9 |
10 | width: 'fit-content',
11 |
12 | userSelect: 'none',
13 | cursor: 'pointer',
14 |
15 | padding: '5px 0px 5px 5px',
16 |
17 | '&:hover': {
18 | backgroundColor: '$accents1',
19 | },
20 |
21 | [`&:hover p`]: {
22 | color: '$text',
23 | },
24 |
25 | variants: {
26 | active: {
27 | true: {
28 | '& p': {
29 | color: '$text',
30 | },
31 | },
32 | false: {
33 | '& p': {
34 | color: '$accents8',
35 | },
36 | },
37 | },
38 |
39 | closable: {
40 | false: {
41 | paddingRight: 5,
42 | },
43 | },
44 | },
45 | });
46 |
47 | export type TabBarItemProps = {
48 | id: string;
49 |
50 | onClick?: () => void;
51 | } & VariantProps;
52 |
--------------------------------------------------------------------------------
/src/app/components/select/select.tsx:
--------------------------------------------------------------------------------
1 | import { CSS, styled, VariantProps } from '@nextui-org/react';
2 | import React from 'react';
3 | import ReactSelect, { Props as ReactSelectProps } from 'react-select';
4 |
5 | import { StyledSelect } from './select.styled';
6 |
7 | export type SelectProps = {
8 | css?: CSS;
9 | } & VariantProps &
10 | ReactSelectProps;
11 |
12 | export function SelectFactory() {
13 | return (props: ReactSelectProps) => ;
14 | }
15 |
16 | export const Select = ({
17 | bordered = false,
18 | separator = false,
19 | size = 'md',
20 | css,
21 | ...props
22 | }: SelectProps) => {
23 | const TypedSelect = React.memo(SelectFactory());
24 | const StyledTypedSelect = React.memo(styled(TypedSelect, StyledSelect));
25 |
26 | return (
27 |
36 | );
37 | };
38 |
--------------------------------------------------------------------------------
/src/app/storage/logs.storage.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-param-reassign */
2 |
3 | import { produce } from 'immer';
4 | import create from 'zustand';
5 | import { persist } from 'zustand/middleware';
6 |
7 | import { LogStorage } from './interfaces';
8 |
9 | export const useLogsStore = create(
10 | persist(
11 | (set) => ({
12 | logs: [],
13 | newLogsAvailable: false,
14 | createLog: (log) =>
15 | set(
16 | produce((state) => {
17 | state.logs.push(log);
18 | state.newLogsAvailable = true;
19 | })
20 | ),
21 | clearLogs: () =>
22 | set(
23 | produce((state) => {
24 | state.logs = [];
25 | state.newLogsAvailable = false;
26 | })
27 | ),
28 | markAsReadLogs: () =>
29 | set(
30 | produce((state) => {
31 | state.newLogsAvailable = false;
32 | })
33 | ),
34 | }),
35 | {
36 | name: 'logs',
37 | getStorage: () => window.electronStore,
38 | }
39 | )
40 | );
41 |
--------------------------------------------------------------------------------
/src/app/context/app.context.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useEffectOnce } from 'react-use';
3 |
4 | export interface IAppContext {
5 | platform: {
6 | os: string;
7 | setOs: (os: string) => void;
8 | };
9 | modal: {
10 | createCollectionModalVisible: boolean;
11 | setCreateCollectionModalVisible: (visible: boolean) => void;
12 | };
13 | }
14 |
15 | export const AppContext = React.createContext(null);
16 |
17 | export const AppProvider = AppContext.Provider;
18 |
19 | export function useAppContextProvider() {
20 | const [createCollectionModalVisible, setCreateCollectionModalVisible] = React.useState(false);
21 | const [os, setOs] = React.useState('darwin');
22 |
23 | useEffectOnce(() => {
24 | window.os.get().then((value) => {
25 | setOs(value);
26 | });
27 | });
28 |
29 | const appContext: IAppContext = {
30 | platform: {
31 | os,
32 | setOs,
33 | },
34 | modal: {
35 | createCollectionModalVisible,
36 | setCreateCollectionModalVisible,
37 | },
38 | };
39 |
40 | return {
41 | appContext,
42 | AppProvider,
43 | };
44 | }
45 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "module": "esnext",
5 | "target": "ESNext",
6 | "lib": [
7 | "DOM",
8 | "DOM.Iterable",
9 | "esnext"
10 | ],
11 | "strict": true,
12 | "skipLibCheck": true,
13 | "esModuleInterop": true,
14 | "noImplicitAny": true,
15 | "sourceMap": true,
16 | "baseUrl": ".",
17 | "paths": {
18 | "@components": [
19 | "./src/app/components/index.ts"
20 | ],
21 | "@hooks": [
22 | "./src/app/hooks/index.ts"
23 | ],
24 | "@storage": [
25 | "./src/app/storage/index.ts"
26 | ],
27 | "@context": [
28 | "./src/app/context/index.ts"
29 | ],
30 | "@layouts": [
31 | "./src/app/layouts/index.ts"
32 | ],
33 | "@core": [
34 | "./src/core/index.ts"
35 | ],
36 | "@core/types": [
37 | "./src/core/typings.ts"
38 | ],
39 | },
40 | "outDir": "dist",
41 | "moduleResolution": "node",
42 | "resolveJsonModule": true,
43 | "jsx": "react",
44 | },
45 | "include": ["src/**/*", "stories", "forge.config.js"],
46 | }
47 |
--------------------------------------------------------------------------------
/src/main/clients/grpc-client/subscribers/unary.subscriber.ts:
--------------------------------------------------------------------------------
1 | import { MetadataValue } from '@grpc/grpc-js';
2 | import { BrowserWindow, IpcMain } from 'electron';
3 |
4 | import { GrpcClient, GrpcClientRequestOptions, GrpcOptions, ProtobufLoader } from '@core';
5 |
6 | import { GrpcClientChannel } from '../constants';
7 |
8 | export class GrpcClientUnarySubscriber {
9 | constructor(private readonly mainWindow: BrowserWindow, private readonly ipcMain: IpcMain) {}
10 |
11 | public static unregisterUnaryCallHandlers(ipcMain: IpcMain) {
12 | ipcMain.removeHandler(GrpcClientChannel.INVOKE_UNARY_REQUEST);
13 | }
14 |
15 | public registerUnaryCallHandlers() {
16 | this.ipcMain.handle(
17 | GrpcClientChannel.INVOKE_UNARY_REQUEST,
18 | async (
19 | _event,
20 | options: GrpcOptions,
21 | requestOptions: GrpcClientRequestOptions,
22 | payload: Record,
23 | metadata?: Record
24 | ) => {
25 | const ast = await ProtobufLoader.loadFromFile(options);
26 |
27 | return GrpcClient.invokeUnaryRequest(ast, requestOptions, payload, metadata);
28 | }
29 | );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/app/components/info-label/info-label.tsx:
--------------------------------------------------------------------------------
1 | import { faCircleInfo } from '@fortawesome/free-solid-svg-icons';
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3 | import { SimpleColors, Spacer, styled, Text, Tooltip } from '@nextui-org/react';
4 | import React from 'react';
5 |
6 | export interface InfoLabelProps {
7 | label: string;
8 | description?: string;
9 |
10 | color?: SimpleColors;
11 | }
12 |
13 | const InfoLabelWrapper = styled('div', {
14 | display: 'flex',
15 | flexWrap: 'nowrap',
16 | alignItems: 'center',
17 | });
18 |
19 | const StyledIcon = styled(FontAwesomeIcon, {
20 | color: '$accents6',
21 | '&:hover': {
22 | color: '$warning',
23 | },
24 | });
25 |
26 | export const InfoLabel: React.FC = ({ label, description, color }) => (
27 |
28 |
29 | {label}
30 |
31 | {description && (
32 | <>
33 |
34 |
35 |
36 |
37 | >
38 | )}
39 |
40 | );
41 |
--------------------------------------------------------------------------------
/__tests__/basic-service/proto/basic_grpc_pb.js:
--------------------------------------------------------------------------------
1 | // GENERATED CODE -- DO NOT EDIT!
2 |
3 | 'use strict';
4 | var grpc = require('@grpc/grpc-js');
5 | var proto_basic_pb = require('../proto/basic_pb.js');
6 |
7 | function serialize_BasicMessage(arg) {
8 | if (!(arg instanceof proto_basic_pb.BasicMessage)) {
9 | throw new Error('Expected argument of type BasicMessage');
10 | }
11 | return Buffer.from(arg.serializeBinary());
12 | }
13 |
14 | function deserialize_BasicMessage(buffer_arg) {
15 | return proto_basic_pb.BasicMessage.deserializeBinary(new Uint8Array(buffer_arg));
16 | }
17 |
18 |
19 | var BasicServiceService = exports.BasicServiceService = {
20 | basicRequest: {
21 | path: '/BasicService/BasicRequest',
22 | requestStream: false,
23 | responseStream: false,
24 | requestType: proto_basic_pb.BasicMessage,
25 | responseType: proto_basic_pb.BasicMessage,
26 | requestSerialize: serialize_BasicMessage,
27 | requestDeserialize: deserialize_BasicMessage,
28 | responseSerialize: serialize_BasicMessage,
29 | responseDeserialize: deserialize_BasicMessage,
30 | },
31 | };
32 |
33 | exports.BasicServiceClient = grpc.makeGenericClientConstructor(BasicServiceService);
34 |
--------------------------------------------------------------------------------
/src/main/clients/grpc-web-client/subscribers/unary.subscriber.ts:
--------------------------------------------------------------------------------
1 | import { BrowserWindow, IpcMain } from 'electron';
2 |
3 | import {
4 | GrpcClientRequestOptions,
5 | GrpcOptions,
6 | GrpcWebClient,
7 | GrpcWebMetadataValue,
8 | ProtobufLoader,
9 | } from '@core';
10 |
11 | import { GrpcWebClientChannel } from '../constants';
12 |
13 | export class GrpcWebClientUnarySubscriber {
14 | constructor(private readonly mainWindow: BrowserWindow, private readonly ipcMain: IpcMain) {}
15 |
16 | public static unregisterUnaryCallHandlers(ipcMain: IpcMain) {
17 | ipcMain.removeHandler(GrpcWebClientChannel.INVOKE_UNARY_REQUEST);
18 | }
19 |
20 | public registerUnaryCallHandlers() {
21 | this.ipcMain.handle(
22 | GrpcWebClientChannel.INVOKE_UNARY_REQUEST,
23 | async (
24 | _event,
25 | options: GrpcOptions,
26 | requestOptions: GrpcClientRequestOptions,
27 | payload: Record,
28 | metadata?: Record
29 | ) => {
30 | const ast = await ProtobufLoader.loadFromFile(options);
31 |
32 | return GrpcWebClient.invokeUnaryRequest(ast, requestOptions, payload, metadata);
33 | }
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/__tests__/tls-service/src/client.ts:
--------------------------------------------------------------------------------
1 | import { ChannelCredentials } from '@grpc/grpc-js';
2 | import * as fs from 'fs';
3 | import * as path from 'path';
4 |
5 | import { TLSServiceClient } from './generated/proto/tls_service';
6 |
7 | function getChannelCredentials(): ChannelCredentials {
8 | const rootCert = fs.readFileSync(path.resolve(__dirname, '../certs/ca-cert.pem'));
9 |
10 | if (process.env.TLS_MODE === 'mutual') {
11 | const clientCert = fs.readFileSync(path.resolve(__dirname, '../certs/client-cert.pem'));
12 | const clientKey = fs.readFileSync(path.resolve(__dirname, '../certs/client-key.pem'));
13 | const channelCredentials = ChannelCredentials.createSsl(rootCert, clientKey, clientCert);
14 |
15 | return channelCredentials;
16 | }
17 |
18 | const channelCredentials = ChannelCredentials.createSsl(rootCert);
19 |
20 | return channelCredentials;
21 | }
22 |
23 | function main() {
24 | const credentials = getChannelCredentials();
25 |
26 | const client = new TLSServiceClient('0.0.0.0:4001', credentials);
27 |
28 | client.unary({ id: 'test' }, (error, response) => {
29 | // eslint-disable-next-line no-console
30 | console.log('response: ', response);
31 | });
32 | }
33 |
34 | main();
35 |
--------------------------------------------------------------------------------
/src/app/pages/logs/logs.button.tsx:
--------------------------------------------------------------------------------
1 | import { faListSquares } from '@fortawesome/free-solid-svg-icons';
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3 | import { Button, ButtonProps, styled } from '@nextui-org/react';
4 | import React from 'react';
5 |
6 | export type LogsButtonProps = ButtonProps & {
7 | badgeVisible: boolean;
8 | };
9 |
10 | const Badge = styled('span', {
11 | position: 'absolute',
12 | left: 25,
13 | top: 17,
14 | backgroundColor: '$error',
15 | br: '$rounded',
16 | width: 7,
17 | height: 7,
18 | zIndex: 2,
19 | });
20 |
21 | export const LogsButton: React.FC = ({ badgeVisible, ...props }) => (
22 |
23 | }
40 | {...props}
41 | />
42 | {badgeVisible && }
43 |
44 | );
45 |
--------------------------------------------------------------------------------
/src/app/storage/settings.storage.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-param-reassign */
2 |
3 | import { produce } from 'immer';
4 | import create from 'zustand';
5 | import { persist } from 'zustand/middleware';
6 |
7 | import { Alignment, Language, SettingsStorage, ThemeType } from './interfaces';
8 |
9 | export const useSettingsStore = create(
10 | persist(
11 | (set) => ({
12 | theme: ThemeType.DARK,
13 | language: Language.EN,
14 | alignment: Alignment.VERTICAL,
15 | isMenuCollapsed: true,
16 |
17 | updateTheme: (theme) =>
18 | set(
19 | produce((state) => {
20 | state.theme = theme;
21 | })
22 | ),
23 |
24 | updateAlignment: (alignment) =>
25 | set(
26 | produce((state) => {
27 | state.alignment = alignment;
28 | })
29 | ),
30 |
31 | setIsMenuCollapsed: (isCollapsed) =>
32 | set(
33 | produce((state) => {
34 | state.isMenuCollapsed = isCollapsed;
35 | })
36 | ),
37 | }),
38 | {
39 | name: 'settings',
40 | getStorage: () => window.electronStore,
41 | }
42 | )
43 | );
44 |
--------------------------------------------------------------------------------
/src/app/hooks/collections/use-update-collection.ts:
--------------------------------------------------------------------------------
1 | import { notification } from '@components';
2 | import { Collection, CollectionType, useCollectionsStore } from '@storage';
3 |
4 | export interface UpdateCollectionOptions {
5 | hideSuccessNotification: boolean;
6 | }
7 |
8 | export function useUpdateCollection() {
9 | const updateCollection = useCollectionsStore((store) => store.updateCollection);
10 |
11 | const update = async (
12 | id: string,
13 | collection: Collection,
14 | options?: UpdateCollectionOptions
15 | ) => {
16 | try {
17 | await updateCollection(id, collection);
18 |
19 | if (!options?.hideSuccessNotification) {
20 | notification(
21 | {
22 | title: `${collection.name}`,
23 | description: 'Collection successfully updated',
24 | },
25 | { type: 'success', position: 'top-right' }
26 | );
27 | }
28 | } catch (error: any) {
29 | notification(
30 | {
31 | title: `${collection.name} sync error`,
32 | description: error?.message,
33 | },
34 | { type: 'error', position: 'top-right' }
35 | );
36 | }
37 | };
38 |
39 | return { update };
40 | }
41 |
--------------------------------------------------------------------------------
/.storybook/preview.js:
--------------------------------------------------------------------------------
1 | import { NextUIProvider } from '@nextui-org/react';
2 |
3 | import { DarkTheme, LightTheme } from '../src/app/themes';
4 |
5 | export const parameters = {
6 | actions: { argTypesRegex: "^on[A-Z].*" },
7 | controls: {
8 | matchers: {
9 | color: /(background|color)$/i,
10 | date: /Date$/,
11 | },
12 | },
13 | }
14 |
15 | export const globalTypes = {
16 | theme: {
17 | name: 'Theme',
18 | description: 'Global theme for components',
19 | defaultValue: 'Dark',
20 | toolbar: {
21 | icon: 'paintbrush',
22 | items: [
23 | 'Light',
24 | 'Dark',
25 | ],
26 | },
27 | },
28 | };
29 |
30 | const THEMES = {
31 | 'Dark': DarkTheme,
32 | 'Light': LightTheme,
33 | };
34 |
35 | function getTheme(theme) {
36 | if (!THEMES[theme]) {
37 | throw new Error(`No theme ${theme}`);
38 | }
39 |
40 | return THEMES[theme];
41 | }
42 |
43 |
44 | const withThemeProvider = (Story, context) => {
45 | const theme = getTheme(context.globals.theme);
46 |
47 | return (
48 | <>
49 |
52 |
53 |
54 | >
55 | );
56 | };
57 |
58 | export const decorators = [withThemeProvider];
59 |
--------------------------------------------------------------------------------
/src/app/pages/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useEffectOnce } from 'react-use';
3 |
4 | import { useAppContextProvider } from '@context';
5 | import { useUpdateCollection } from '@hooks';
6 | import { DefaultLayout } from '@layouts';
7 | import { useCollectionsStore } from '@storage';
8 |
9 | import { Shortcuts } from './shortcuts';
10 | import { SideBar } from './side-bar';
11 | import { StatusBar } from './status-bar';
12 | import { TabsContainer } from './tabs-container';
13 |
14 | export const Main = (): JSX.Element => {
15 | const { collections } = useCollectionsStore((store) => store);
16 | const { appContext, AppProvider } = useAppContextProvider();
17 | const { update: updateCollection } = useUpdateCollection();
18 |
19 | useEffectOnce(() => {
20 | // Hack for loading toast styles first
21 | setTimeout(() => {
22 | collections.forEach((collection) => {
23 | updateCollection(collection.id, collection, { hideSuccessNotification: true });
24 | });
25 | }, 0);
26 | });
27 |
28 | return (
29 |
30 |
31 | } bottom={}>
32 |
33 |
34 |
35 |
36 | );
37 | };
38 |
--------------------------------------------------------------------------------
/src/app/pages/tabs-container/welcome/welcome-container.tsx:
--------------------------------------------------------------------------------
1 | import { Col, Container, Row, Spacer, 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 WelcomeContainer: React.FC = () => {
8 | const { getShortcuts } = useShortcuts();
9 |
10 | const shortcuts = getShortcuts(ShortcutsGroup.WELCOME);
11 |
12 | return (
13 |
21 |
22 | {shortcuts.map((shortcut) => (
23 |
24 |
25 |
26 | {shortcut.description}
27 |
28 |
29 |
30 |
31 | {shortcut.key}
32 |
33 |
34 |
35 |
36 | ))}
37 |
38 |
39 | );
40 | };
41 |
--------------------------------------------------------------------------------
/src/app/components/select/colored/colored-select.tsx:
--------------------------------------------------------------------------------
1 | import { styled } from '@nextui-org/react';
2 | import React from 'react';
3 |
4 | import { SelectFactory, SelectProps } from '../select';
5 | import { StyledSelect } from '../select.styled';
6 | import { ColoredOption } from './colored-option';
7 | import { ColoredSingleValue } from './colored-single-value';
8 | import { ColoredSelectOption } from './interfaces';
9 |
10 | export type ColoredSelectProps = SelectProps & {
11 | onRemove?: (item: ColoredSelectOption) => void;
12 | };
13 |
14 | const TypedColoredSelect = SelectFactory();
15 | const StyledColoredSelect = styled(TypedColoredSelect, StyledSelect);
16 |
17 | export const ColoredSelect: React.FC = ({
18 | bordered = false,
19 | separator = false,
20 | size = 'md',
21 | css,
22 | ...props
23 | }) => (
24 | option.id}
33 | components={{
34 | SingleValue: ColoredSingleValue,
35 | // @ts-ignore
36 | Option: ColoredOption,
37 | }}
38 | {...props}
39 | />
40 | );
41 |
--------------------------------------------------------------------------------
/src/app/pages/tabs-container/collection-types/grpc/send-header/send-header.unary-call.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Loading, Spacer } from '@nextui-org/react';
2 | import React from 'react';
3 |
4 | import { GrpcMethodType } from '@core/types';
5 | import { useGrpcTabContextStore, useUnaryCall } from '@hooks';
6 |
7 | import { SendHeader, SendHeaderProps } from './send-header.basic';
8 |
9 | export const UnaryCallSendHeader: React.FC> = ({ tab }) => {
10 | const { invoke } = useUnaryCall();
11 | const { getContext } = useGrpcTabContextStore();
12 |
13 | const context = getContext(tab.id);
14 |
15 | const handleInvokeButtonClick = () => invoke(tab);
16 |
17 | return (
18 |
19 |
20 |
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 |
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 |
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 | onClick={handleRemoveButtonClick}
43 | />
44 | );
45 |
46 | return (
47 |
48 | );
49 | };
50 |
--------------------------------------------------------------------------------
/src/app/pages/settings/modals/update-settings.modal.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Modal, ModalProps, Text } from '@nextui-org/react';
2 | import React from 'react';
3 |
4 | import { Settings, useSettingsStore } from '@storage';
5 |
6 | import { SettingsForm } from '../forms';
7 |
8 | export const UpdateSettingsModal: React.FC = ({ onClose = () => {}, ...props }) => {
9 | const { updateTheme, theme } = useSettingsStore((store) => store);
10 |
11 | const handleSubmit = async (payload: Partial) => {
12 | if (payload.theme) {
13 | await updateTheme(payload.theme);
14 | }
15 |
16 | onClose();
17 | };
18 |
19 | return (
20 |
26 |
27 | Settings
28 |
29 |
30 |
37 |
38 |
39 |
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 |
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 | }
44 | onClick={handleCancelButtonClick}
45 | />
46 | >
47 | )}
48 |
49 |
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 |
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 | }
72 | onClick={handleClearButtonClick}
73 | >
74 | Clear
75 |
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 | }
55 | onClick={handleCopyButtonClick}
56 | />
57 |
58 | }
71 | onClick={closeToast}
72 | />
73 |
74 |
75 | {description && {description}}
76 |
77 | );
78 | };
79 |
--------------------------------------------------------------------------------
/src/app/components/file-input/file.input.tsx:
--------------------------------------------------------------------------------
1 | import { faPlus } from '@fortawesome/free-solid-svg-icons';
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3 | import { Container, Input, InputProps, NormalColors, Spacer, Text } from '@nextui-org/react';
4 | import React from 'react';
5 |
6 | import { EzyButton } from '@components';
7 |
8 | export type FileInputProps = Partial> & {
9 | buttonColor?: NormalColors;
10 | value?: string | null | undefined;
11 |
12 | onChange: (path: string | undefined) => void;
13 | };
14 |
15 | export const FileInput = React.forwardRef(
16 | ({ accept, buttonColor, onChange, value, readOnly, label, color, ...props }, ref) => {
17 | const [inputValue, setInputValue] = React.useState(value);
18 |
19 | React.useEffect(() => {
20 | setInputValue(value);
21 | }, [value]);
22 |
23 | const handleDialogOpenButtonClick = async () => {
24 | const paths = await window.electronDialog.open({ properties: ['openFile'] });
25 |
26 | if (Array.isArray(paths) && paths.length) {
27 | setInputValue(paths[0]);
28 | onChange(paths[0]);
29 | }
30 | };
31 |
32 | return (
33 | onChange(undefined)}
39 | color={color}
40 | // @ts-ignore
41 | label={
42 |
43 |
44 | {label}
45 |
46 |
47 | }
53 | css={{ minWidth: 10, color: '$accents8', borderColor: '$accents3' }}
54 | onClick={handleDialogOpenButtonClick}
55 | />
56 |
57 | }
58 | css={{
59 | '.nextui-input': {
60 | cursor: 'default',
61 | },
62 | '.nextui-input-clear-button': {
63 | cursor: 'pointer',
64 | },
65 | }}
66 | {...props}
67 | />
68 | );
69 | }
70 | );
71 |
--------------------------------------------------------------------------------
/src/app/pages/tabs-container/collection-types/grpc/response/streams/stream-icons.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | faArrowLeft,
3 | faArrowRight,
4 | faCheck,
5 | faPlay,
6 | faStop,
7 | faXmark,
8 | } from '@fortawesome/free-solid-svg-icons';
9 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
10 | import { styled, Text } from '@nextui-org/react';
11 | import React from 'react';
12 |
13 | import { GrpcStreamMessageType } from '@storage';
14 |
15 | const IconWrapper = styled('div', {
16 | minWidth: 12,
17 | maxWidth: 12,
18 | });
19 |
20 | export const StreamIcons = {
21 | [GrpcStreamMessageType.CLIENT_MESSAGE]: (
22 |
23 |
24 |
25 | ),
26 | [GrpcStreamMessageType.SERVER_MESSAGE]: (
27 |
28 |
29 |
30 | ),
31 | [GrpcStreamMessageType.STARTED]: (
32 |
33 |
34 |
35 | ),
36 | [GrpcStreamMessageType.CLIENT_STREAMING_ENDED]: (
37 |
38 |
39 |
40 | ),
41 | [GrpcStreamMessageType.SERVER_STREAMING_ENDED]: (
42 |
43 |
44 |
45 | ),
46 | [GrpcStreamMessageType.CANCELED]: (
47 |
48 |
49 |
50 | ),
51 | [GrpcStreamMessageType.ERROR]: (
52 |
53 |
54 |
55 | ),
56 | };
57 |
58 | export const StreamMessageTypeText = {
59 | [GrpcStreamMessageType.CLIENT_MESSAGE]: Client message,
60 | [GrpcStreamMessageType.SERVER_MESSAGE]: Server message,
61 | [GrpcStreamMessageType.STARTED]: Stream started,
62 | [GrpcStreamMessageType.CLIENT_STREAMING_ENDED]: Client streaming ended,
63 | [GrpcStreamMessageType.SERVER_STREAMING_ENDED]: Server streaming ended,
64 | [GrpcStreamMessageType.CANCELED]: Stream canceled,
65 | [GrpcStreamMessageType.ERROR]: Stream error,
66 | };
67 |
--------------------------------------------------------------------------------
/src/app/components/menu/menu.tsx:
--------------------------------------------------------------------------------
1 | import { styled } from '@nextui-org/react';
2 | import { nanoid } from 'nanoid';
3 | import React from 'react';
4 |
5 | import { MenuItem, MenuItemData } from './menu-item';
6 |
7 | const StyledMenu = styled('div', {
8 | display: 'flex',
9 | flexDirection: 'column',
10 | flex: 1,
11 |
12 | width: 50,
13 | maxWidth: 50,
14 |
15 | backgroundColor: '$background',
16 |
17 | borderRight: 'solid 0.1px $border',
18 | });
19 |
20 | const SubMenu = styled('div', {
21 | display: 'flex',
22 | flexWrap: 'nowrap',
23 | transition: 'all 0.2s ease',
24 |
25 | borderRight: 'solid 0.1px $border',
26 |
27 | variants: {
28 | isCollapsed: {
29 | true: {
30 | '& div': {
31 | display: 'none',
32 | },
33 | width: 0,
34 | },
35 | false: {
36 | width: 250,
37 | minWidth: 250,
38 | },
39 | },
40 | },
41 | });
42 |
43 | const TopContainer = styled('div', {
44 | borderBottom: 'solid 0.1px $border',
45 | });
46 |
47 | const BottomContainer = styled('div', {
48 | marginTop: 'auto',
49 | });
50 |
51 | export interface MenuProps {
52 | items: MenuItemData[];
53 |
54 | top?: React.ReactNode;
55 |
56 | bottom?: React.ReactNode;
57 |
58 | isCollapsed?: boolean;
59 |
60 | onCollapseChange?: (isCollapsed: boolean) => void;
61 | }
62 |
63 | export const Menu: React.FC = ({
64 | items,
65 | bottom,
66 | top,
67 | isCollapsed = false,
68 | onCollapseChange,
69 | }) => {
70 | const [activeItem, setActiveItem] = React.useState(items[0].id);
71 |
72 | const handleMenuItemClick = (index: string) => {
73 | if (index === activeItem && onCollapseChange) {
74 | onCollapseChange(!isCollapsed);
75 | } else {
76 | setActiveItem(index);
77 | }
78 | };
79 |
80 | const submenu = items.find((item) => item.id === activeItem)?.submenu;
81 |
82 | return (
83 | <>
84 |
85 | {top}
86 | {items.map((item) => (
87 |
98 | {!!submenu && submenu}
99 | >
100 | );
101 | };
102 |
--------------------------------------------------------------------------------
/src/app/pages/collections/forms/collection.form.tsx:
--------------------------------------------------------------------------------
1 | import { Container, Input, Spacer } from '@nextui-org/react';
2 | import React from 'react';
3 | import { Controller, useForm } from 'react-hook-form';
4 |
5 | import { FileInput } from '@components';
6 | import { Collection, CollectionType } from '@storage';
7 |
8 | import { IncludeDirectoriesField } from './fields';
9 |
10 | export interface CollectionFormProps {
11 | id?: string;
12 |
13 | defaultValues?: Partial>;
14 |
15 | onSubmit: (payload: Collection) => void;
16 | }
17 |
18 | export const CollectionForm: React.FC = ({
19 | onSubmit = () => {},
20 | id,
21 | defaultValues,
22 | }) => {
23 | const {
24 | control,
25 | register,
26 | reset,
27 | handleSubmit,
28 | formState: { errors },
29 | } = useForm>({ defaultValues });
30 |
31 | React.useEffect(() => {
32 | reset();
33 | }, [defaultValues]);
34 |
35 | return (
36 |
88 | );
89 | };
90 |
--------------------------------------------------------------------------------
/src/app/pages/shortcuts/shortcuts.tsx:
--------------------------------------------------------------------------------
1 | import { faArrowsRotate, faSquarePlus, faXmark } from '@fortawesome/free-solid-svg-icons';
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3 | import { createAction, KBarProvider } from '@getezy/kbar';
4 | import React, { PropsWithChildren } from 'react';
5 |
6 | import { KBar } from '@components';
7 | import { AppContext } from '@context';
8 | import { useUpdateCollection } from '@hooks';
9 | import { useCollectionsStore, useTabsStore } from '@storage';
10 |
11 | import { useEnvironmentActions, useGrpcMethodActions, useThemeActions } from './hooks';
12 |
13 | interface ActionsProviderProps {
14 | os: string;
15 | }
16 |
17 | const ActionsProvider: React.FC> = ({ children, os }) => {
18 | useGrpcMethodActions();
19 | useEnvironmentActions();
20 | useThemeActions();
21 |
22 | return {children};
23 | };
24 |
25 | export const Shortcuts: React.FC = ({ children }) => {
26 | const context = React.useContext(AppContext);
27 |
28 | const { closeAllTabs, closeActiveTab } = useTabsStore((store) => store);
29 | const { collections } = useCollectionsStore((store) => store);
30 | const { update: updateCollection } = useUpdateCollection();
31 |
32 | const actions = [
33 | createAction({
34 | section: 'Collections',
35 | name: 'New Collection',
36 | icon: ,
37 | shortcut: ['$mod+Shift+C'],
38 | perform: () => context?.modal.setCreateCollectionModalVisible(true),
39 | }),
40 | createAction({
41 | section: 'Collections',
42 | name: 'Synchronize All',
43 | icon: ,
44 | shortcut: ['$mod+Shift+S'],
45 | perform: () => {
46 | collections.forEach((collection) => {
47 | updateCollection(collection.id, collection);
48 | });
49 | },
50 | }),
51 | createAction({
52 | section: 'Tabs',
53 | name: 'Close Active Tab',
54 | icon: ,
55 | shortcut: ['$mod+W'],
56 | perform: () => closeActiveTab(),
57 | }),
58 | createAction({
59 | section: 'Tabs',
60 | name: 'Close All Tabs',
61 | icon: ,
62 | shortcut: ['$mod+Shift+W'],
63 | perform: () => closeAllTabs(),
64 | }),
65 | ];
66 |
67 | return (
68 |
69 | {children}
70 |
71 | );
72 | };
73 |
--------------------------------------------------------------------------------
/__tests__/simple-service/src/generated/google/protobuf/empty.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import Long from "long";
3 | import _m0 from "protobufjs/minimal";
4 |
5 | export const protobufPackage = "google.protobuf";
6 |
7 | /**
8 | * A generic empty message that you can re-use to avoid defining duplicated
9 | * empty messages in your APIs. A typical example is to use it as the request
10 | * or the response type of an API method. For instance:
11 | *
12 | * service Foo {
13 | * rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty);
14 | * }
15 | *
16 | * The JSON representation for `Empty` is empty JSON object `{}`.
17 | */
18 | export interface Empty {}
19 |
20 | function createBaseEmpty(): Empty {
21 | return {};
22 | }
23 |
24 | export const Empty = {
25 | encode(_: Empty, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
26 | return writer;
27 | },
28 |
29 | decode(input: _m0.Reader | Uint8Array, length?: number): Empty {
30 | const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input);
31 | let end = length === undefined ? reader.len : reader.pos + length;
32 | const message = createBaseEmpty();
33 | while (reader.pos < end) {
34 | const tag = reader.uint32();
35 | switch (tag >>> 3) {
36 | default:
37 | reader.skipType(tag & 7);
38 | break;
39 | }
40 | }
41 | return message;
42 | },
43 |
44 | fromJSON(_: any): Empty {
45 | return {};
46 | },
47 |
48 | toJSON(_: Empty): unknown {
49 | const obj: any = {};
50 | return obj;
51 | },
52 |
53 | fromPartial, I>>(_: I): Empty {
54 | const message = createBaseEmpty();
55 | return message;
56 | },
57 | };
58 |
59 | type Builtin =
60 | | Date
61 | | Function
62 | | Uint8Array
63 | | string
64 | | number
65 | | boolean
66 | | undefined;
67 |
68 | export type DeepPartial = T extends Builtin
69 | ? T
70 | : T extends Long
71 | ? string | number | Long
72 | : T extends Array
73 | ? Array>
74 | : T extends ReadonlyArray
75 | ? ReadonlyArray>
76 | : T extends {}
77 | ? { [K in keyof T]?: DeepPartial }
78 | : Partial;
79 |
80 | type KeysOfUnion = T extends T ? keyof T : never;
81 | export type Exact = P extends Builtin
82 | ? P
83 | : P & { [K in keyof P]: Exact
} & Record<
84 | Exclude>,
85 | never
86 | >;
87 |
88 | if (_m0.util.Long !== Long) {
89 | _m0.util.Long = Long as any;
90 | _m0.configure();
91 | }
92 |
--------------------------------------------------------------------------------