= ({ badgeVisible, ...props }) => (
-
- }
- {...props}
- />
- {badgeVisible && }
-
-);
diff --git a/src/app/pages/logs/logs.modal.tsx b/src/app/pages/logs/logs.modal.tsx
deleted file mode 100644
index d36d6612..00000000
--- a/src/app/pages/logs/logs.modal.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-import { faBroomBall } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Button, Modal, ModalProps, styled, Text } from '@nextui-org/react';
-import { nanoid } from 'nanoid';
-import React from 'react';
-
-import { useLogsStore } from '@storage';
-
-const LogItem = styled('div', {
- paddingLeft: 10,
- paddingRight: 10,
- '&:hover': {
- backgroundColor: '$backgroundContrast',
- },
-});
-
-export const LogsModal: React.FC = ({ onClose = () => {}, ...props }) => {
- const { logs, clearLogs } = useLogsStore((store) => store);
-
- const content =
- logs.length > 0 ? (
- logs.map((log) => (
-
-
- {log.message}
-
-
- ))
- ) : (
-
- No logs
-
- );
-
- const handleClearButtonClick = () => {
- clearLogs();
- };
-
- return (
-
-
- Logs
-
-
- {content}
-
-
- }
- onClick={handleClearButtonClick}
- >
- Clear
-
-
-
-
- );
-};
diff --git a/src/app/storage/environments.storage.ts b/src/app/storage/environments.storage.ts
deleted file mode 100644
index 2525b086..00000000
--- a/src/app/storage/environments.storage.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { produce } from 'immer';
-import create from 'zustand';
-import { persist } from 'zustand/middleware';
-
-import { EnvironmentsStorage } from './interfaces';
-
-export const useEnvironmentsStore = create(
- persist(
- (set) => ({
- environments: [],
- createEnvironment: (environment) =>
- set(
- produce((state) => {
- state.environments.push(environment);
- })
- ),
- removeEnvironment: (id) =>
- set(
- produce((state) => {
- const index = state.environments.findIndex((environment) => environment.id === id);
- if (index !== -1) state.environments.splice(index, 1);
- })
- ),
- }),
- {
- name: 'environments',
- getStorage: () => window.electronStore,
- }
- )
-);
diff --git a/src/app/storage/interfaces/environments.interface.ts b/src/app/storage/interfaces/environments.interface.ts
deleted file mode 100644
index 8c58b2fd..00000000
--- a/src/app/storage/interfaces/environments.interface.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-export interface Environment {
- id: string;
- label: string;
- url: string;
- color: string;
-}
-
-export interface EnvironmentsStorage {
- environments: Environment[];
-
- createEnvironment: (environment: Environment) => void;
- removeEnvironment: (id: string) => void;
-}
diff --git a/src/app/storage/interfaces/index.ts b/src/app/storage/interfaces/index.ts
deleted file mode 100644
index 5ef02567..00000000
--- a/src/app/storage/interfaces/index.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export * from './settings.interface';
-export * from './collections.interface';
-export * from './environments.interface';
-export * from './logs.interface';
-export * from './tls-presets.interface';
-export * from './tabs';
diff --git a/src/app/storage/interfaces/logs.interface.ts b/src/app/storage/interfaces/logs.interface.ts
deleted file mode 100644
index a9c7299b..00000000
--- a/src/app/storage/interfaces/logs.interface.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-export interface LogItem {
- message: string;
-}
-
-export interface LogStorage {
- logs: LogItem[];
- newLogsAvailable: boolean;
-
- createLog: (log: LogItem) => void;
- clearLogs: () => void;
- markAsReadLogs: () => void;
-}
diff --git a/src/app/storage/interfaces/settings.interface.ts b/src/app/storage/interfaces/settings.interface.ts
deleted file mode 100644
index e7ee6056..00000000
--- a/src/app/storage/interfaces/settings.interface.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-export enum Language {
- EN = 'en',
-}
-
-export enum ThemeType {
- DARK = 'dark',
- LIGHT = 'light',
-}
-
-export enum Alignment {
- VERTICAL = 'vertical',
- HORIZONTAL = 'horizontal',
-}
-
-export interface Settings {
- theme: ThemeType;
- language: Language;
- alignment: Alignment;
- isMenuCollapsed: boolean;
-}
-
-export interface SettingsStorage extends Settings {
- updateTheme: (theme: ThemeType) => void;
- updateAlignment: (alignment: Alignment) => void;
- setIsMenuCollapsed: (isCollapsed: boolean) => void;
-}
diff --git a/src/app/storage/interfaces/tls-presets.interface.ts b/src/app/storage/interfaces/tls-presets.interface.ts
deleted file mode 100644
index 3817a841..00000000
--- a/src/app/storage/interfaces/tls-presets.interface.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { GrpcTlsConfig, GrpcTlsType } from '@core/types';
-
-export interface TlsPreset {
- id: string;
- name: string;
- system: boolean;
- tls: GrpcTlsConfig;
-}
-
-export interface TlsPresetsStorage {
- presets: TlsPreset[];
-
- createTlsPreset: (preset: Omit) => void;
- updateTlsPreset: (id: string, preset: Omit) => void;
- removeTlsPreset: (id: string) => void;
-}
diff --git a/src/app/storage/logs.storage.ts b/src/app/storage/logs.storage.ts
deleted file mode 100644
index 92cf9657..00000000
--- a/src/app/storage/logs.storage.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-/* eslint-disable no-param-reassign */
-
-import { produce } from 'immer';
-import create from 'zustand';
-import { persist } from 'zustand/middleware';
-
-import { LogStorage } from './interfaces';
-
-export const useLogsStore = create(
- persist(
- (set) => ({
- logs: [],
- newLogsAvailable: false,
- createLog: (log) =>
- set(
- produce((state) => {
- state.logs.push(log);
- state.newLogsAvailable = true;
- })
- ),
- clearLogs: () =>
- set(
- produce((state) => {
- state.logs = [];
- state.newLogsAvailable = false;
- })
- ),
- markAsReadLogs: () =>
- set(
- produce((state) => {
- state.newLogsAvailable = false;
- })
- ),
- }),
- {
- name: 'logs',
- getStorage: () => window.electronStore,
- }
- )
-);
diff --git a/src/app/storage/settings.storage.ts b/src/app/storage/settings.storage.ts
deleted file mode 100644
index f824c039..00000000
--- a/src/app/storage/settings.storage.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-/* eslint-disable no-param-reassign */
-
-import { produce } from 'immer';
-import create from 'zustand';
-import { persist } from 'zustand/middleware';
-
-import { Alignment, Language, SettingsStorage, ThemeType } from './interfaces';
-
-export const useSettingsStore = create(
- persist(
- (set) => ({
- theme: ThemeType.DARK,
- language: Language.EN,
- alignment: Alignment.VERTICAL,
- isMenuCollapsed: true,
-
- updateTheme: (theme) =>
- set(
- produce((state) => {
- state.theme = theme;
- })
- ),
-
- updateAlignment: (alignment) =>
- set(
- produce((state) => {
- state.alignment = alignment;
- })
- ),
-
- setIsMenuCollapsed: (isCollapsed) =>
- set(
- produce((state) => {
- state.isMenuCollapsed = isCollapsed;
- })
- ),
- }),
- {
- name: 'settings',
- getStorage: () => window.electronStore,
- }
- )
-);
diff --git a/src/app/storage/tls-presets.storage.ts b/src/app/storage/tls-presets.storage.ts
deleted file mode 100644
index f0dfd881..00000000
--- a/src/app/storage/tls-presets.storage.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-/* eslint-disable no-param-reassign */
-
-import { produce } from 'immer';
-import { nanoid } from 'nanoid';
-import create from 'zustand';
-import { persist } from 'zustand/middleware';
-
-import { GrpcTlsType } from '@core/types';
-
-import { TlsPresetsStorage } from './interfaces';
-
-export const useTlsPresetsStore = create(
- persist(
- (set) => ({
- presets: [
- {
- id: nanoid(),
- name: 'Insecure',
- system: true,
- tls: { type: GrpcTlsType.INSECURE },
- },
- {
- id: nanoid(),
- name: 'Server-side',
- system: true,
- tls: { type: GrpcTlsType.SERVER_SIDE },
- },
- ],
- createTlsPreset: (preset) =>
- set(
- produce((state) => {
- state.presets.push({
- system: false,
- ...preset,
- });
- })
- ),
- updateTlsPreset: (id, preset) =>
- set(
- produce((state) => {
- const index = state.presets.findIndex((item) => item.id === id);
-
- if (index !== -1) {
- state.presets[index] = {
- ...state.presets[index],
- ...preset,
- };
- }
- })
- ),
- removeTlsPreset: (id) =>
- set(
- produce((state) => {
- const index = state.presets.findIndex((preset) => preset.id === id);
- if (index !== -1) state.presets.splice(index, 1);
- })
- ),
- }),
- {
- name: 'tls-presets',
- getStorage: () => window.electronStore,
- }
- )
-);
diff --git a/src/core/__tests__/fixtures/proto/basic.proto b/src/core/__tests__/fixtures/proto/basic.proto
deleted file mode 100644
index 6ec1128f..00000000
--- a/src/core/__tests__/fixtures/proto/basic.proto
+++ /dev/null
@@ -1,9 +0,0 @@
-syntax = "proto3";
-
-message BasicMessage {
- string id = 1;
-}
-
-service BasicService {
- rpc BasicRequest(BasicMessage) returns (BasicMessage);
-}
diff --git a/src/core/__tests__/fixtures/proto/simple.proto b/src/core/__tests__/fixtures/proto/simple.proto
deleted file mode 100644
index 22a0e61f..00000000
--- a/src/core/__tests__/fixtures/proto/simple.proto
+++ /dev/null
@@ -1,21 +0,0 @@
-syntax = "proto3";
-
-package simple_package.v1;
-
-import "google/protobuf/empty.proto";
-
-message SimpleMessage {
- string id = 1;
-}
-
-service SimpleService {
- rpc SimpleUnaryRequest(SimpleMessage) returns (google.protobuf.Empty);
-
- rpc SimpleClientStreamRequest(stream SimpleMessage) returns (SimpleMessage);
-
- rpc SimpleServerStreamRequest(google.protobuf.Empty)
- returns (stream SimpleMessage);
-
- rpc SimpleBidirectionalStreamRequest(stream SimpleMessage)
- returns (stream SimpleMessage);
-}
diff --git a/src/core/clients/grpc/grpc-client/__tests__/grpc-client.spec.ts b/src/core/clients/grpc/grpc-client/__tests__/grpc-client.spec.ts
deleted file mode 100644
index 435de183..00000000
--- a/src/core/clients/grpc/grpc-client/__tests__/grpc-client.spec.ts
+++ /dev/null
@@ -1,224 +0,0 @@
-import * as grpc from '@grpc/grpc-js';
-// import { ClientReadableStreamImpl } from '@grpc/grpc-js/build/src/call';
-import { join } from 'path';
-
-import { ProtobufLoader } from '../../../../protobuf';
-import { GrpcClientRequestOptions, GrpcStatus, GrpcTlsType } from '../../interfaces';
-import { GrpcClient } from '../grpc-client';
-
-function createBasicService(error: any, response: any) {
- const BasicService = jest.fn(() => ({
- BasicRequest: jest.fn((_payload, _metadata, callback) => {
- callback(error, response);
- }),
- }));
-
- // @ts-ignore
- BasicService.serviceName = 'BasicService';
-
- return BasicService;
-}
-
-function createSimpleService(error: any, response: any) {
- const SimpleService = jest.fn(() => ({
- SimpleUnaryRequest: jest.fn((_payload, _metadata, callback) => {
- callback(error, response);
- }),
- // SimpleServerStreamRequest: jest.fn(() => new ClientReadableStreamImpl(jest.fn())),
- }));
-
- // @ts-ignore
- SimpleService.serviceName = 'simple_package.v1.SimpleService';
-
- return SimpleService;
-}
-
-describe('GrpcClient', () => {
- describe('GrpcClient::InvokeUnaryRequest', () => {
- it('should invoke unary request', async () => {
- const packageDefinition = await ProtobufLoader.loadFromFile({
- path: join(__dirname, '../../../../__tests__/fixtures/proto/basic.proto'),
- });
-
- const requestOptions: GrpcClientRequestOptions = {
- serviceName: 'BasicService',
- methodName: 'BasicRequest',
- address: '127.0.0.1:3000',
- tls: { type: GrpcTlsType.INSECURE },
- };
-
- const payload = {
- id: 'testid',
- };
-
- const BasicService = createBasicService(null, payload);
-
- jest.spyOn(grpc, 'loadPackageDefinition').mockImplementationOnce(() => ({
- // @ts-ignore
- BasicService,
- }));
-
- await expect(
- GrpcClient.invokeUnaryRequest(packageDefinition, requestOptions, payload)
- ).resolves.toEqual({
- code: GrpcStatus.OK,
- timestamp: 0,
- value: payload,
- });
- });
-
- it('should invoke unary request width metadata', async () => {
- const packageDefinition = await ProtobufLoader.loadFromFile({
- path: join(__dirname, '../../../../__tests__/fixtures/proto/basic.proto'),
- });
-
- const requestOptions: GrpcClientRequestOptions = {
- serviceName: 'BasicService',
- methodName: 'BasicRequest',
- address: '127.0.0.1:3000',
- tls: { type: GrpcTlsType.INSECURE },
- };
-
- const payload = {
- id: 'testid',
- };
-
- const metadata = {
- 'x-user-token': 'token',
- };
-
- const BasicService = createBasicService(null, payload);
-
- jest.spyOn(grpc, 'loadPackageDefinition').mockImplementationOnce(() => ({
- // @ts-ignore
- BasicService,
- }));
-
- await expect(
- GrpcClient.invokeUnaryRequest(packageDefinition, requestOptions, payload, metadata)
- ).resolves.toEqual({
- code: GrpcStatus.OK,
- timestamp: 0,
- value: payload,
- });
- });
-
- it('should invoke unary request with error', async () => {
- const packageDefinition = await ProtobufLoader.loadFromFile({
- path: join(__dirname, '../../../../__tests__/fixtures/proto/basic.proto'),
- });
-
- const requestOptions: GrpcClientRequestOptions = {
- serviceName: 'BasicService',
- methodName: 'BasicRequest',
- address: '127.0.0.1:3000',
- tls: { type: GrpcTlsType.INSECURE },
- };
-
- const payload = {
- id: 'testid',
- };
-
- const error = { code: GrpcStatus.UNAVAILABLE, details: 'No connection established' };
-
- const BasicService = createBasicService(error, null);
-
- jest.spyOn(grpc, 'loadPackageDefinition').mockImplementationOnce(() => ({
- // @ts-ignore
- BasicService,
- }));
-
- await expect(
- GrpcClient.invokeUnaryRequest(packageDefinition, requestOptions, payload)
- ).resolves.toEqual({
- code: GrpcStatus.UNAVAILABLE,
- timestamp: 0,
- value: error,
- });
- });
-
- it('should invoke unary request with package definition', async () => {
- const packageDefinition = await ProtobufLoader.loadFromFile({
- path: join(__dirname, '../../../../__tests__/fixtures/proto/simple.proto'),
- });
-
- const requestOptions: GrpcClientRequestOptions = {
- serviceName: 'simple_package.v1.SimpleService',
- methodName: 'SimpleUnaryRequest',
- address: '127.0.0.1:3000',
- tls: { type: GrpcTlsType.INSECURE },
- };
-
- const payload = {
- id: 'testid',
- };
-
- const SimpleService = createSimpleService(null, payload);
-
- jest.spyOn(grpc, 'loadPackageDefinition').mockImplementationOnce(() => ({
- // @ts-ignore
- 'simple_package.v1.SimpleService': SimpleService,
- }));
-
- await expect(
- GrpcClient.invokeUnaryRequest(packageDefinition, requestOptions, payload)
- ).resolves.toEqual({
- code: GrpcStatus.OK,
- timestamp: 0,
- value: payload,
- });
- });
-
- it('should throw error when no service definition exist', async () => {
- const packageDefinition = await ProtobufLoader.loadFromFile({
- path: join(__dirname, '../../../../__tests__/fixtures/proto/simple.proto'),
- });
-
- const requestOptions: GrpcClientRequestOptions = {
- serviceName: 'SomeService',
- methodName: 'SomeRequest',
- address: '127.0.0.1:3000',
- tls: { type: GrpcTlsType.INSECURE },
- };
-
- await expect(
- GrpcClient.invokeUnaryRequest(packageDefinition, requestOptions, {})
- ).rejects.toThrowError('No service definition');
- });
-
- it('should throw error when no method definition exist', async () => {
- const packageDefinition = await ProtobufLoader.loadFromFile({
- path: join(__dirname, '../../../../__tests__/fixtures/proto/simple.proto'),
- });
-
- const requestOptions: GrpcClientRequestOptions = {
- serviceName: 'simple_package.v1.SimpleService',
- methodName: 'SomeRequest',
- address: '127.0.0.1:3000',
- tls: { type: GrpcTlsType.INSECURE },
- };
-
- await expect(
- GrpcClient.invokeUnaryRequest(packageDefinition, requestOptions, {})
- ).rejects.toThrowError('No method definition');
- });
- });
-
- // describe('GrpcClient::InvokeServerStreamingRequest', () => {
- // it('should invoke server streaming request', async () => {
- // const packageDefinition = await ProtobufLoader.loadFromFile({
- // path: join(__dirname, '../../../__tests__/fixtures/proto/simple.proto'),
- // });
-
- // const requestOptions: GrpcClientRequestOptions = {
- // serviceName: 'simple_package.v1.SimpleService',
- // methodName: 'SimpleServerStreamRequest',
- // address: '127.0.0.1:3000',
- // };
-
- // const call = GrpcClient.invokeServerStreamingRequest(packageDefinition, requestOptions, {});
-
- // expect(call instanceof ClientReadableStreamImpl).toBe(true);
- // });
- // });
-});
diff --git a/src/core/clients/grpc/grpc-client/__tests__/metadata-parser.spec.ts b/src/core/clients/grpc/grpc-client/__tests__/metadata-parser.spec.ts
deleted file mode 100644
index 23ec5cf3..00000000
--- a/src/core/clients/grpc/grpc-client/__tests__/metadata-parser.spec.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { MetadataParser } from '../metadata-parser';
-
-describe('MetadataParser', () => {
- it('should parse metadata when value defined', () => {
- const value = { test: '123' };
-
- const metadata = MetadataParser.parse(value);
-
- expect(metadata.get('test')).toEqual(['123']);
- });
-});
diff --git a/src/core/clients/grpc/grpc-client/grpc-client.ts b/src/core/clients/grpc/grpc-client/grpc-client.ts
index 43ee9dc6..f31494ae 100644
--- a/src/core/clients/grpc/grpc-client/grpc-client.ts
+++ b/src/core/clients/grpc/grpc-client/grpc-client.ts
@@ -21,8 +21,8 @@ import {
GrpcStatus,
GrpcTlsConfig,
GrpcTlsType,
- isInsecureTlsConfig,
isMutualTlsConfig,
+ isServerSideTlsConfig,
} from '../interfaces';
import { MetadataParser } from './metadata-parser';
@@ -34,17 +34,17 @@ export class GrpcClient {
private static getChannelCredentials(tls: GrpcTlsConfig): ChannelCredentials {
let credentials: ChannelCredentials;
- if (isInsecureTlsConfig(tls)) {
- credentials = grpc.credentials.createInsecure();
- } else if (isMutualTlsConfig(tls)) {
+ if (isMutualTlsConfig(tls)) {
const rootCert = tls.rootCertificatePath ? fs.readFileSync(tls.rootCertificatePath) : null;
const clientCert = fs.readFileSync(tls.clientCertificatePath);
const clientKey = fs.readFileSync(tls.clientKeyPath);
credentials = grpc.credentials.createSsl(rootCert, clientKey, clientCert);
- } else {
+ } else if (isServerSideTlsConfig(tls)) {
const rootCert = tls.rootCertificatePath ? fs.readFileSync(tls.rootCertificatePath) : null;
credentials = grpc.credentials.createSsl(rootCert);
+ } else {
+ credentials = grpc.credentials.createInsecure();
}
return credentials;
diff --git a/src/core/clients/grpc/grpc-web-client/grpc-web-call.stream.ts b/src/core/clients/grpc/grpc-web-client/grpc-web-call.stream.ts
index 366eb910..3137b218 100644
--- a/src/core/clients/grpc/grpc-web-client/grpc-web-call.stream.ts
+++ b/src/core/clients/grpc/grpc-web-client/grpc-web-call.stream.ts
@@ -34,7 +34,7 @@ export class GrpcWebCallStream extends EventEmitter {
this.call = grpc.invoke(this.methodDefinition, {
...this.options,
transport: NodeHttpTransport({
- ...httpsOptions,
+ ...this.httpsOptions,
}),
onMessage: (responseMessage) => {
this.emit('message', responseMessage);
diff --git a/src/core/clients/grpc/grpc-web-client/grpc-web-client.ts b/src/core/clients/grpc/grpc-web-client/grpc-web-client.ts
index 9448b539..180f485e 100644
--- a/src/core/clients/grpc/grpc-web-client/grpc-web-client.ts
+++ b/src/core/clients/grpc/grpc-web-client/grpc-web-client.ts
@@ -13,6 +13,7 @@ import {
GrpcTlsType,
isInsecureTlsConfig,
isMutualTlsConfig,
+ isServerSideTlsConfig,
} from '../interfaces';
import { GrpcWebCallStream } from './grpc-web-call.stream';
import { GrpcWebMetadataValue } from './interfaces';
@@ -67,11 +68,9 @@ export class GrpcWebClient {
}
private static getRequestOptions(tls: GrpcTlsConfig): https.RequestOptions {
- let options: https.RequestOptions;
+ let options: https.RequestOptions = {};
- if (isInsecureTlsConfig(tls)) {
- options = {};
- } else if (isMutualTlsConfig(tls)) {
+ if (isMutualTlsConfig(tls)) {
const rootCert = tls.rootCertificatePath
? fs.readFileSync(tls.rootCertificatePath)
: undefined;
@@ -83,7 +82,7 @@ export class GrpcWebClient {
cert: clientCert,
key: clientKey,
};
- } else {
+ } else if (isServerSideTlsConfig(tls)) {
const rootCert = tls.rootCertificatePath
? fs.readFileSync(tls.rootCertificatePath)
: undefined;
diff --git a/src/core/clients/grpc/interfaces/client.interface.ts b/src/core/clients/grpc/interfaces/client.interface.ts
index ca0b8a16..662cea21 100644
--- a/src/core/clients/grpc/interfaces/client.interface.ts
+++ b/src/core/clients/grpc/interfaces/client.interface.ts
@@ -4,7 +4,7 @@ export enum GrpcTlsType {
MUTUAL = 'mutual',
}
-export type GrpcTlsConfig = T extends GrpcTlsType.MUTUAL
+export type GrpcTlsConfig = T extends GrpcTlsType.MUTUAL
? GrpcMutualTlsConfig
: T extends GrpcTlsType.SERVER_SIDE
? GrpcServerSideTlsConfig
@@ -28,7 +28,7 @@ export interface GrpcChannelOptions {
}
export interface GrpcServerSideTlsConfig {
- type: GrpcTlsType.SERVER_SIDE;
+ type: GrpcTlsType;
rootCertificatePath?: string;
@@ -36,7 +36,7 @@ export interface GrpcServerSideTlsConfig {
}
export interface GrpcMutualTlsConfig {
- type: GrpcTlsType.MUTUAL;
+ type: GrpcTlsType;
rootCertificatePath?: string;
@@ -48,7 +48,7 @@ export interface GrpcMutualTlsConfig {
}
export interface GrpcInsecureTlsConfig {
- type: GrpcTlsType.INSECURE;
+ type: GrpcTlsType;
channelOptions?: GrpcChannelOptions;
}
@@ -69,6 +69,12 @@ export function isInsecureTlsConfig(
return config.type === GrpcTlsType.INSECURE;
}
+export function isServerSideTlsConfig(
+ config: GrpcTlsConfig
+): config is GrpcTlsConfig {
+ return config.type === GrpcTlsType.SERVER_SIDE;
+}
+
export function isMutualTlsConfig(
config: GrpcTlsConfig
): config is GrpcTlsConfig {
diff --git a/src/core/entities/__tests__/environment.entity.spec.ts b/src/core/entities/__tests__/environment.entity.spec.ts
new file mode 100644
index 00000000..dbf98d50
--- /dev/null
+++ b/src/core/entities/__tests__/environment.entity.spec.ts
@@ -0,0 +1,16 @@
+import { Environment } from '../environments';
+
+describe('Environment', () => {
+ it('should create new environment with automatically generated id', () => {
+ const environment = Environment.create({ label: 'label', url: 'url', color: 'color' });
+
+ expect(environment).toStrictEqual(
+ new Environment({
+ id: expect.anything(),
+ label: 'label',
+ url: 'url',
+ color: 'color',
+ })
+ );
+ });
+});
diff --git a/src/core/entities/__tests__/setting.entity.spec.ts b/src/core/entities/__tests__/setting.entity.spec.ts
new file mode 100644
index 00000000..161c0b92
--- /dev/null
+++ b/src/core/entities/__tests__/setting.entity.spec.ts
@@ -0,0 +1,30 @@
+import { Alignment, Language, Setting, SettingKey, Theme } from '../settings';
+
+describe('Setting', () => {
+ it('should create new alignment setting', () => {
+ const setting = new Setting({
+ key: SettingKey.ALIGNMENT,
+ value: Alignment.HORIZONTAL,
+ });
+
+ expect(setting.value).toStrictEqual(Alignment.HORIZONTAL);
+ });
+
+ it('should create new theme setting', () => {
+ const setting = new Setting({ key: SettingKey.THEME, value: Theme.DARK });
+
+ expect(setting.value).toStrictEqual(Theme.DARK);
+ });
+
+ it('should create new language setting', () => {
+ const setting = new Setting({ key: SettingKey.LANGUAGE, value: Language.EN });
+
+ expect(setting.value).toStrictEqual(Language.EN);
+ });
+
+ it('should create new menu setting', () => {
+ const setting = new Setting({ key: SettingKey.MENU, value: { collapsed: true } });
+
+ expect(setting.value).toStrictEqual({ collapsed: true });
+ });
+});
diff --git a/src/core/entities/__tests__/tls-preset.entity.spec.ts b/src/core/entities/__tests__/tls-preset.entity.spec.ts
new file mode 100644
index 00000000..d4befab6
--- /dev/null
+++ b/src/core/entities/__tests__/tls-preset.entity.spec.ts
@@ -0,0 +1,22 @@
+import { GrpcTlsType } from '@getezy/grpc-client';
+
+import { TlsPreset } from '../tls-presets';
+
+describe('TlsPreset', () => {
+ it('should create new tls preset with automatically generated id', () => {
+ const preset = TlsPreset.create({
+ name: 'name',
+ system: false,
+ tls: { type: GrpcTlsType.INSECURE },
+ });
+
+ expect(preset).toStrictEqual(
+ new TlsPreset({
+ id: expect.anything(),
+ name: 'name',
+ system: false,
+ tls: { type: GrpcTlsType.INSECURE },
+ })
+ );
+ });
+});
diff --git a/src/core/entities/collections/__tests__/abstract-collection.entity.spec.ts b/src/core/entities/collections/__tests__/abstract-collection.entity.spec.ts
new file mode 100644
index 00000000..0f07b1eb
--- /dev/null
+++ b/src/core/entities/collections/__tests__/abstract-collection.entity.spec.ts
@@ -0,0 +1,31 @@
+import { AbstractCollection } from '../abstract-collection.entity';
+import { CollectionType } from '../collection-type.enum';
+
+class TestCollection extends AbstractCollection {}
+
+describe('AbstractCollection', () => {
+ let testCollection: TestCollection;
+
+ beforeEach(() => {
+ testCollection = new TestCollection({
+ id: '123',
+ name: 'test-collection',
+ type: CollectionType.Collection,
+ });
+ });
+
+ test('should have an id property', () => {
+ expect(testCollection.id).toBeDefined();
+ expect(testCollection.id).toBe('123');
+ });
+
+ test('should have a name property', () => {
+ expect(testCollection.name).toBeDefined();
+ expect(testCollection.name).toBe('test-collection');
+ });
+
+ test('should have a type property', () => {
+ expect(testCollection.type).toBeDefined();
+ expect(testCollection.type).toBe(CollectionType.Collection);
+ });
+});
diff --git a/src/core/entities/collections/abstract-collection.entity.ts b/src/core/entities/collections/abstract-collection.entity.ts
new file mode 100644
index 00000000..b8040176
--- /dev/null
+++ b/src/core/entities/collections/abstract-collection.entity.ts
@@ -0,0 +1,21 @@
+import { CollectionType } from './collection-type.enum';
+
+export interface IAbstractCollection {
+ id: string;
+ name: string;
+ type: CollectionType;
+}
+
+export abstract class AbstractCollection implements IAbstractCollection {
+ public id: string;
+
+ public name: string;
+
+ public type: CollectionType;
+
+ constructor({ id, name, type }: IAbstractCollection) {
+ this.id = id;
+ this.name = name;
+ this.type = type;
+ }
+}
diff --git a/src/core/entities/collections/collection-type.enum.ts b/src/core/entities/collections/collection-type.enum.ts
new file mode 100644
index 00000000..b6a8955d
--- /dev/null
+++ b/src/core/entities/collections/collection-type.enum.ts
@@ -0,0 +1,4 @@
+export enum CollectionType {
+ Collection = 'collection',
+ Grpc = 'grpc',
+}
diff --git a/src/core/entities/collections/collection.entity.ts b/src/core/entities/collections/collection.entity.ts
new file mode 100644
index 00000000..9fa8c383
--- /dev/null
+++ b/src/core/entities/collections/collection.entity.ts
@@ -0,0 +1,24 @@
+import { AbstractCollection, IAbstractCollection } from './abstract-collection.entity';
+import { isGrpcCollection } from './collection.guards';
+import { CollectionType } from './collection-type.enum';
+import { GrpcCollection } from './grpc';
+
+export type ICollection = {
+ children: AbstractCollection[];
+} & Pick;
+
+export class Collection extends AbstractCollection implements ICollection {
+ public children: AbstractCollection[];
+
+ constructor({ id, name, children }: ICollection) {
+ super({ id, name, type: CollectionType.Collection });
+
+ this.children = children.map((child) => {
+ if (isGrpcCollection(child)) {
+ return new GrpcCollection(child);
+ }
+
+ return new Collection(child as Collection);
+ });
+ }
+}
diff --git a/src/core/entities/collections/collection.guards.ts b/src/core/entities/collections/collection.guards.ts
new file mode 100644
index 00000000..e6b1e02e
--- /dev/null
+++ b/src/core/entities/collections/collection.guards.ts
@@ -0,0 +1,7 @@
+import { AbstractCollection } from './abstract-collection.entity';
+import { CollectionType } from './collection-type.enum';
+import { GrpcCollection } from './grpc';
+
+export function isGrpcCollection(value: AbstractCollection): value is GrpcCollection {
+ return value.type === CollectionType.Grpc;
+}
diff --git a/src/core/entities/collections/grpc/grpc-collection.entity.ts b/src/core/entities/collections/grpc/grpc-collection.entity.ts
new file mode 100644
index 00000000..9b76e37e
--- /dev/null
+++ b/src/core/entities/collections/grpc/grpc-collection.entity.ts
@@ -0,0 +1,22 @@
+import { GrpcOptions } from '@core';
+
+import { AbstractCollection, IAbstractCollection } from '../abstract-collection.entity';
+import { CollectionType } from '../collection-type.enum';
+import { GrpcService, IGrpcService } from './grpc-service.entity';
+
+export type IGrpcCollection = {
+ options: GrpcOptions;
+ services: IGrpcService[];
+} & Pick;
+
+export class GrpcCollection extends AbstractCollection implements IGrpcCollection {
+ public options: GrpcOptions;
+
+ public services: GrpcService[];
+
+ constructor({ id, name, options, services }: IGrpcCollection) {
+ super({ id, name, type: CollectionType.Grpc });
+ this.options = options;
+ this.services = services.map((service) => new GrpcService(service));
+ }
+}
diff --git a/src/core/entities/collections/grpc/grpc-method.entity.ts b/src/core/entities/collections/grpc/grpc-method.entity.ts
new file mode 100644
index 00000000..eabbb132
--- /dev/null
+++ b/src/core/entities/collections/grpc/grpc-method.entity.ts
@@ -0,0 +1,19 @@
+import { GrpcMethodDefinition, GrpcMethodType } from '@core';
+
+export type IGrpcMethod = {
+ id?: string;
+} & GrpcMethodDefinition;
+
+export class GrpcMethod implements IGrpcMethod {
+ public id?: string;
+
+ public name: string;
+
+ public type: GrpcMethodType;
+
+ constructor({ id, name, type }: IGrpcMethod) {
+ this.id = id;
+ this.name = name;
+ this.type = type;
+ }
+}
diff --git a/src/core/entities/collections/grpc/grpc-service.entity.ts b/src/core/entities/collections/grpc/grpc-service.entity.ts
new file mode 100644
index 00000000..9c8405e4
--- /dev/null
+++ b/src/core/entities/collections/grpc/grpc-service.entity.ts
@@ -0,0 +1,21 @@
+import { GrpcServiceDefinition } from '@core';
+
+import { GrpcMethod } from './grpc-method.entity';
+
+export type IGrpcService = {
+ id?: string;
+} & GrpcServiceDefinition;
+
+export class GrpcService implements IGrpcService {
+ public id?: string;
+
+ public name: string;
+
+ public methods: GrpcMethod[];
+
+ constructor({ id, name, methods }: IGrpcService) {
+ this.id = id;
+ this.name = name;
+ this.methods = methods.map((method) => new GrpcMethod(method));
+ }
+}
diff --git a/src/core/entities/collections/grpc/index.ts b/src/core/entities/collections/grpc/index.ts
new file mode 100644
index 00000000..9f773238
--- /dev/null
+++ b/src/core/entities/collections/grpc/index.ts
@@ -0,0 +1 @@
+export * from './grpc-collection.entity';
diff --git a/src/core/entities/collections/index.ts b/src/core/entities/collections/index.ts
new file mode 100644
index 00000000..093be970
--- /dev/null
+++ b/src/core/entities/collections/index.ts
@@ -0,0 +1,2 @@
+// TODO: change imports to @core in this folder
+export const a = {};
diff --git a/src/core/entities/environments/environment.entity.ts b/src/core/entities/environments/environment.entity.ts
new file mode 100644
index 00000000..93b229ea
--- /dev/null
+++ b/src/core/entities/environments/environment.entity.ts
@@ -0,0 +1,32 @@
+import { AutoMap } from '@automapper/classes';
+import { v4 as uuid } from 'uuid';
+
+import { IEnvironment } from './interfaces';
+
+export class Environment implements IEnvironment {
+ @AutoMap()
+ id: string;
+
+ @AutoMap()
+ label: string;
+
+ @AutoMap()
+ url: string;
+
+ @AutoMap()
+ color: string;
+
+ static create(data: Omit) {
+ return new Environment({
+ id: uuid(),
+ ...data,
+ });
+ }
+
+ constructor({ id, label, url, color }: IEnvironment) {
+ this.id = id;
+ this.label = label;
+ this.url = url;
+ this.color = color;
+ }
+}
diff --git a/src/core/entities/environments/index.ts b/src/core/entities/environments/index.ts
new file mode 100644
index 00000000..fabab7bc
--- /dev/null
+++ b/src/core/entities/environments/index.ts
@@ -0,0 +1,2 @@
+export * from './environment.entity';
+export * from './interfaces';
diff --git a/src/core/entities/environments/interfaces/environment.interface.ts b/src/core/entities/environments/interfaces/environment.interface.ts
new file mode 100644
index 00000000..bf3e8c8c
--- /dev/null
+++ b/src/core/entities/environments/interfaces/environment.interface.ts
@@ -0,0 +1,6 @@
+export interface IEnvironment {
+ id: string;
+ label: string;
+ url: string;
+ color: string;
+}
diff --git a/src/core/entities/environments/interfaces/index.ts b/src/core/entities/environments/interfaces/index.ts
new file mode 100644
index 00000000..3af98079
--- /dev/null
+++ b/src/core/entities/environments/interfaces/index.ts
@@ -0,0 +1 @@
+export * from './environment.interface';
diff --git a/src/core/entities/index.ts b/src/core/entities/index.ts
new file mode 100644
index 00000000..02c087fb
--- /dev/null
+++ b/src/core/entities/index.ts
@@ -0,0 +1,5 @@
+export * from './tls-presets';
+export * from './settings';
+export * from './environments';
+export * from './collections';
+export * from './tabs';
diff --git a/src/core/entities/settings/index.ts b/src/core/entities/settings/index.ts
new file mode 100644
index 00000000..5883f778
--- /dev/null
+++ b/src/core/entities/settings/index.ts
@@ -0,0 +1,2 @@
+export * from './setting.entity';
+export * from './interfaces';
diff --git a/src/core/entities/settings/interfaces/alignment.enum.ts b/src/core/entities/settings/interfaces/alignment.enum.ts
new file mode 100644
index 00000000..13050824
--- /dev/null
+++ b/src/core/entities/settings/interfaces/alignment.enum.ts
@@ -0,0 +1,4 @@
+export enum Alignment {
+ VERTICAL = 'vertical',
+ HORIZONTAL = 'horizontal',
+}
diff --git a/src/core/entities/settings/interfaces/index.ts b/src/core/entities/settings/interfaces/index.ts
new file mode 100644
index 00000000..742da3c8
--- /dev/null
+++ b/src/core/entities/settings/interfaces/index.ts
@@ -0,0 +1,7 @@
+export * from './alignment.enum';
+export * from './key.enum';
+export * from './language.enum';
+export * from './settings.guards';
+export * from './theme.enum';
+export * from './value.interface';
+export * from './setting.interface';
diff --git a/src/core/entities/settings/interfaces/key.enum.ts b/src/core/entities/settings/interfaces/key.enum.ts
new file mode 100644
index 00000000..b3e4b944
--- /dev/null
+++ b/src/core/entities/settings/interfaces/key.enum.ts
@@ -0,0 +1,6 @@
+export enum SettingKey {
+ THEME = 'theme',
+ LANGUAGE = 'language',
+ ALIGNMENT = 'alignment',
+ MENU = 'menu',
+}
diff --git a/src/core/entities/settings/interfaces/language.enum.ts b/src/core/entities/settings/interfaces/language.enum.ts
new file mode 100644
index 00000000..3c269780
--- /dev/null
+++ b/src/core/entities/settings/interfaces/language.enum.ts
@@ -0,0 +1,3 @@
+export enum Language {
+ EN = 'en',
+}
diff --git a/src/core/entities/settings/interfaces/setting.interface.ts b/src/core/entities/settings/interfaces/setting.interface.ts
new file mode 100644
index 00000000..77e3b0ea
--- /dev/null
+++ b/src/core/entities/settings/interfaces/setting.interface.ts
@@ -0,0 +1,8 @@
+import { SettingKey } from './key.enum';
+import { SettingValue } from './value.interface';
+
+export interface ISetting {
+ key: Key;
+
+ value: SettingValue;
+}
diff --git a/src/core/entities/settings/interfaces/settings.guards.ts b/src/core/entities/settings/interfaces/settings.guards.ts
new file mode 100644
index 00000000..8d8c0c00
--- /dev/null
+++ b/src/core/entities/settings/interfaces/settings.guards.ts
@@ -0,0 +1,17 @@
+import { Setting, SettingKey } from '@core';
+
+export function isAlignmentSetting(value: any): value is Setting {
+ return value?.key === SettingKey.ALIGNMENT;
+}
+
+export function isLanguageSetting(value: any): value is Setting {
+ return value?.key === SettingKey.LANGUAGE;
+}
+
+export function isMenuOptionsSetting(value: any): value is Setting {
+ return value?.key === SettingKey.MENU;
+}
+
+export function isThemeSetting(value: any): value is Setting {
+ return value?.key === SettingKey.THEME;
+}
diff --git a/src/core/entities/settings/interfaces/theme.enum.ts b/src/core/entities/settings/interfaces/theme.enum.ts
new file mode 100644
index 00000000..b5e88b63
--- /dev/null
+++ b/src/core/entities/settings/interfaces/theme.enum.ts
@@ -0,0 +1,4 @@
+export enum Theme {
+ DARK = 'dark',
+ LIGHT = 'light',
+}
diff --git a/src/core/entities/settings/interfaces/value.interface.ts b/src/core/entities/settings/interfaces/value.interface.ts
new file mode 100644
index 00000000..01427457
--- /dev/null
+++ b/src/core/entities/settings/interfaces/value.interface.ts
@@ -0,0 +1,18 @@
+import { Alignment } from './alignment.enum';
+import { SettingKey } from './key.enum';
+import { Language } from './language.enum';
+import { Theme } from './theme.enum';
+
+export interface MenuOptions {
+ collapsed: boolean;
+}
+
+export type SettingValue = Key extends SettingKey.ALIGNMENT
+ ? Alignment
+ : Key extends SettingKey.LANGUAGE
+ ? Language
+ : Key extends SettingKey.MENU
+ ? MenuOptions
+ : Key extends SettingKey.THEME
+ ? Theme
+ : never;
diff --git a/src/core/entities/settings/setting.entity.ts b/src/core/entities/settings/setting.entity.ts
new file mode 100644
index 00000000..69395903
--- /dev/null
+++ b/src/core/entities/settings/setting.entity.ts
@@ -0,0 +1,15 @@
+import { AutoMap } from '@automapper/classes';
+
+import type { ISetting, SettingKey, SettingValue } from './interfaces';
+
+export class Setting implements ISetting {
+ @AutoMap()
+ key: Key;
+
+ value: SettingValue;
+
+ constructor({ key, value }: ISetting) {
+ this.key = key;
+ this.value = value;
+ }
+}
diff --git a/src/core/entities/tabs/__tests__/grpc-request/grpc-request-tab.spec.ts b/src/core/entities/tabs/__tests__/grpc-request/grpc-request-tab.spec.ts
new file mode 100644
index 00000000..ff17270f
--- /dev/null
+++ b/src/core/entities/tabs/__tests__/grpc-request/grpc-request-tab.spec.ts
@@ -0,0 +1,18 @@
+import { GrpcProtocolType, GrpcRequestTab, TabType } from '@core';
+
+describe('GrpcRequestTab', () => {
+ it('shoud update GrpcRequestTab', () => {
+ const tab = new GrpcRequestTab({
+ id: '1',
+ active: true,
+ order: 1,
+ title: 'TestTab',
+ type: TabType.GrpcRequest,
+ protocol: GrpcProtocolType.Grpc,
+ url: '10.10.10.10',
+ environmentId: '1',
+ });
+
+ tab.update({});
+ });
+});
diff --git a/src/core/entities/tabs/__tests__/tabs-container.spec.ts b/src/core/entities/tabs/__tests__/tabs-container.spec.ts
new file mode 100644
index 00000000..4b73d72e
--- /dev/null
+++ b/src/core/entities/tabs/__tests__/tabs-container.spec.ts
@@ -0,0 +1,187 @@
+import {
+ GrpcProtocolType,
+ GrpcRequestTab,
+ ICreateTabPayload,
+ IGrpcRequestTab,
+ TabsContainer,
+ TabType,
+} from '@core';
+
+const TAB_PAYLOAD: IGrpcRequestTab = {
+ id: '1',
+ active: true,
+ order: 1,
+ title: 'TestTab',
+ type: TabType.GrpcRequest,
+ protocol: GrpcProtocolType.Grpc,
+ url: '10.10.10.10',
+ environmentId: '1',
+};
+
+describe('TabsContainer', () => {
+ let container: TabsContainer;
+
+ it('should create empty tab container', () => {
+ container = new TabsContainer();
+
+ expect(container.getTabs()).toEqual([]);
+ });
+
+ it('should create tab container with tab', () => {
+ container = new TabsContainer([TAB_PAYLOAD]);
+
+ expect(container.getTabs()).toEqual([TAB_PAYLOAD]);
+ });
+
+ it('should throw error on tab container creation if unsuported tab type was passed', () => {
+ expect(() => new TabsContainer([{} as IGrpcRequestTab])).toThrow();
+ });
+
+ it('should create new tab', () => {
+ container = new TabsContainer();
+
+ const tab = container.createTab(TAB_PAYLOAD);
+
+ expect(tab).toEqual(
+ expect.objectContaining({
+ id: expect.anything(),
+ order: 1,
+ active: true,
+ })
+ );
+ });
+
+ it('should throw error on new tab creation if unsuported tab type was passed', () => {
+ container = new TabsContainer();
+
+ expect(() => container.createTab({} as ICreateTabPayload)).toThrowError('Unsuported tab type.');
+ });
+
+ it('should activate tab', () => {
+ container = new TabsContainer();
+
+ const firstTab = container.createTab(TAB_PAYLOAD);
+ const secondTab = container.createTab(TAB_PAYLOAD);
+
+ container.activateTab(firstTab.id);
+
+ expect(firstTab.active).toEqual(true);
+ expect(secondTab.active).toEqual(false);
+ });
+
+ it('should close active tab', () => {
+ container = new TabsContainer();
+
+ container.createTab(TAB_PAYLOAD);
+ container.createTab(TAB_PAYLOAD);
+ container.createTab(TAB_PAYLOAD);
+
+ container.closeActiveTab();
+
+ const tabs = container.getTabs();
+
+ expect(tabs[0].active).toEqual(false);
+ expect(tabs[1].active).toEqual(true);
+ expect(tabs[2]).toBeUndefined();
+ });
+
+ it('should close tab', () => {
+ container = new TabsContainer();
+
+ container.createTab(TAB_PAYLOAD);
+ const secondTab = container.createTab(TAB_PAYLOAD);
+ const thirdTab = container.createTab(TAB_PAYLOAD);
+
+ container.closeTab(secondTab.id);
+
+ const tabs = container.getTabs();
+
+ expect(tabs.length).toEqual(2);
+ expect(tabs[0]).toEqual(
+ expect.objectContaining({
+ active: false,
+ order: 1,
+ })
+ );
+ expect(tabs[1]).toEqual(
+ expect.objectContaining({
+ id: thirdTab.id,
+ active: true,
+ order: 2,
+ })
+ );
+ });
+
+ it('should close all tabs', () => {
+ container = new TabsContainer();
+
+ container.createTab(TAB_PAYLOAD);
+ container.createTab(TAB_PAYLOAD);
+ container.createTab(TAB_PAYLOAD);
+
+ container.closeAllTabs();
+
+ const tabs = container.getTabs();
+
+ expect(tabs.length).toEqual(0);
+ });
+
+ it('should move tab', () => {
+ container = new TabsContainer();
+
+ const firstTab = container.createTab(TAB_PAYLOAD);
+ const secondTab = container.createTab(TAB_PAYLOAD);
+ const thirdTab = container.createTab(TAB_PAYLOAD);
+
+ container.moveTab(firstTab.id, thirdTab.id);
+
+ const tabs = container.getTabs();
+
+ expect(tabs[0]).toEqual(
+ expect.objectContaining({
+ id: secondTab.id,
+ order: 1,
+ })
+ );
+ expect(tabs[1]).toEqual(
+ expect.objectContaining({
+ id: thirdTab.id,
+ order: 2,
+ })
+ );
+ expect(tabs[2]).toEqual(
+ expect.objectContaining({
+ id: firstTab.id,
+ order: 3,
+ })
+ );
+ });
+
+ it('should reset environment', () => {
+ container = new TabsContainer();
+
+ const firstTab = container.createTab(TAB_PAYLOAD);
+ const secondTab = container.createTab(TAB_PAYLOAD);
+ const thirdTab = container.createTab({ ...TAB_PAYLOAD, environmentId: '2' });
+
+ container.resetEnvironment('1');
+
+ expect(firstTab.environmentId).toEqual(undefined);
+ expect(secondTab.environmentId).toEqual(undefined);
+ expect(thirdTab.environmentId).toEqual('2');
+ });
+
+ it('should update tabs', () => {
+ container = new TabsContainer();
+
+ const firstTab = container.createTab(TAB_PAYLOAD);
+ const secondTab = container.createTab(TAB_PAYLOAD);
+ const thirdTab = container.createTab({ ...TAB_PAYLOAD, environmentId: '2' });
+
+ container.updateTabs([firstTab.id, secondTab.id], { url: 'test' });
+
+ expect(firstTab.url).toEqual('test');
+ expect(secondTab.url).toEqual('test');
+ expect(thirdTab.url).toEqual('10.10.10.10');
+ });
+});
diff --git a/src/core/entities/tabs/abstract-tab.entity.ts b/src/core/entities/tabs/abstract-tab.entity.ts
new file mode 100644
index 00000000..ab36f97d
--- /dev/null
+++ b/src/core/entities/tabs/abstract-tab.entity.ts
@@ -0,0 +1,30 @@
+import { AutoMap } from '@automapper/classes';
+
+import type { IAbstractTab, TabType } from './interfaces';
+
+export abstract class AbstractTab implements IAbstractTab {
+ @AutoMap()
+ public id: string;
+
+ @AutoMap()
+ public title: string;
+
+ @AutoMap()
+ public type: TabType;
+
+ @AutoMap()
+ public active: boolean;
+
+ @AutoMap()
+ public order: number;
+
+ constructor({ id, title, active, type, order }: IAbstractTab) {
+ this.id = id;
+ this.title = title;
+ this.type = type;
+ this.active = active;
+ this.order = order;
+ }
+
+ abstract update(paylod: Partial): void;
+}
diff --git a/src/core/entities/tabs/grpc-request/grpc-request-tab.entity.ts b/src/core/entities/tabs/grpc-request/grpc-request-tab.entity.ts
new file mode 100644
index 00000000..ab764bfe
--- /dev/null
+++ b/src/core/entities/tabs/grpc-request/grpc-request-tab.entity.ts
@@ -0,0 +1,74 @@
+import { AutoMap } from '@automapper/classes';
+import { SetOptional } from 'type-fest';
+
+import { GrpcProtocolType, TabType } from '@core';
+
+import { AbstractTab } from '../abstract-tab.entity';
+import { IAbstractTab } from '../interfaces';
+
+export type IGrpcRequestTab = IAbstractTab & {
+ type: TabType.GrpcRequest;
+
+ protocol: GrpcProtocolType;
+
+ url?: string;
+
+ environmentId?: string;
+
+ tlsId?: string;
+};
+
+export type IGrpcRequestTabCreate = SetOptional;
+
+export class GrpcRequestTab extends AbstractTab implements IGrpcRequestTab {
+ @AutoMap()
+ protocol: GrpcProtocolType;
+
+ @AutoMap()
+ url?: string;
+
+ @AutoMap()
+ environmentId?: string;
+
+ @AutoMap()
+ tlsId?: string;
+
+ constructor({
+ protocol = GrpcProtocolType.Grpc,
+ url,
+ environmentId,
+ ...base
+ }: IGrpcRequestTabCreate) {
+ super(base);
+
+ this.protocol = protocol;
+ this.url = url;
+ this.environmentId = environmentId;
+ }
+
+ update(payload: Partial) {
+ if (Object.hasOwn(payload, 'order') && payload.order !== undefined) {
+ this.order = payload.order;
+ }
+
+ if (Object.hasOwn(payload, 'active') && payload.active !== undefined) {
+ this.active = payload.active;
+ }
+
+ if (Object.hasOwn(payload, 'protocol') && payload.protocol !== undefined) {
+ this.protocol = payload.protocol;
+ }
+
+ if (Object.hasOwn(payload, 'url')) {
+ this.url = payload.url;
+ }
+
+ if (Object.hasOwn(payload, 'environmentId')) {
+ this.environmentId = payload.environmentId;
+ }
+
+ if (Object.hasOwn(payload, 'tlsId')) {
+ this.tlsId = payload.tlsId;
+ }
+ }
+}
diff --git a/src/core/entities/tabs/grpc-request/index.ts b/src/core/entities/tabs/grpc-request/index.ts
new file mode 100644
index 00000000..e8aabb9b
--- /dev/null
+++ b/src/core/entities/tabs/grpc-request/index.ts
@@ -0,0 +1 @@
+export * from './grpc-request-tab.entity';
diff --git a/src/core/entities/tabs/index.ts b/src/core/entities/tabs/index.ts
new file mode 100644
index 00000000..e215b209
--- /dev/null
+++ b/src/core/entities/tabs/index.ts
@@ -0,0 +1,4 @@
+export * from './interfaces';
+export * from './abstract-tab.entity';
+export * from './grpc-request';
+export * from './tabs-container';
diff --git a/src/core/entities/tabs/interfaces/abstract-tab.interface.ts b/src/core/entities/tabs/interfaces/abstract-tab.interface.ts
new file mode 100644
index 00000000..ec51ab42
--- /dev/null
+++ b/src/core/entities/tabs/interfaces/abstract-tab.interface.ts
@@ -0,0 +1,9 @@
+import { TabType } from './tab-type.enum';
+
+export interface IAbstractTab {
+ id: string;
+ title: string;
+ type: TabType;
+ active: boolean;
+ order: number;
+}
diff --git a/src/core/entities/tabs/interfaces/index.ts b/src/core/entities/tabs/interfaces/index.ts
new file mode 100644
index 00000000..e61fd950
--- /dev/null
+++ b/src/core/entities/tabs/interfaces/index.ts
@@ -0,0 +1,3 @@
+export * from './abstract-tab.interface';
+export * from './tab-type.enum';
+export * from './tabs.guards';
diff --git a/src/core/entities/tabs/interfaces/tab-type.enum.ts b/src/core/entities/tabs/interfaces/tab-type.enum.ts
new file mode 100644
index 00000000..75ef3c3e
--- /dev/null
+++ b/src/core/entities/tabs/interfaces/tab-type.enum.ts
@@ -0,0 +1,3 @@
+export enum TabType {
+ GrpcRequest = 'grpc-request',
+}
diff --git a/src/core/entities/tabs/interfaces/tabs.guards.ts b/src/core/entities/tabs/interfaces/tabs.guards.ts
new file mode 100644
index 00000000..0f156b9e
--- /dev/null
+++ b/src/core/entities/tabs/interfaces/tabs.guards.ts
@@ -0,0 +1,5 @@
+import { IAbstractTab, IGrpcRequestTab, TabType } from '@core';
+
+export function isGrpcRequestTab(value?: IAbstractTab): value is IGrpcRequestTab {
+ return value?.type === TabType.GrpcRequest;
+}
diff --git a/src/core/entities/tabs/tabs-container.ts b/src/core/entities/tabs/tabs-container.ts
new file mode 100644
index 00000000..ad693dd7
--- /dev/null
+++ b/src/core/entities/tabs/tabs-container.ts
@@ -0,0 +1,125 @@
+import { arrayMove } from '@dnd-kit/sortable';
+import { v4 as uuid } from 'uuid';
+
+import { AbstractTab } from './abstract-tab.entity';
+import { GrpcRequestTab, IGrpcRequestTabCreate } from './grpc-request';
+import { IAbstractTab, isGrpcRequestTab } from './interfaces';
+
+export type ICreateTabPayload = Omit;
+export type IUpdateTabPayload = Partial>;
+
+export class TabsContainer {
+ private tabs: AbstractTab[];
+
+ constructor(tabs: IAbstractTab[] = []) {
+ this.tabs = tabs.map((tab) => {
+ if (isGrpcRequestTab(tab)) {
+ return new GrpcRequestTab(tab);
+ }
+
+ throw new Error('Unsuported tab type.');
+ });
+ }
+
+ public createTab(payload: ICreateTabPayload): T {
+ let tab: AbstractTab;
+
+ const tabPayload = {
+ ...payload,
+ id: uuid(),
+ order: this.tabs.length + 1,
+ active: false,
+ };
+
+ if (isGrpcRequestTab(tabPayload)) {
+ tab = new GrpcRequestTab(tabPayload);
+ } else {
+ throw new Error('Unsuported tab type.');
+ }
+
+ this.tabs.push(tab);
+
+ this.activateTab(tab.id);
+
+ return tab as T;
+ }
+
+ public updateTabs(ids: string[], payload: IUpdateTabPayload) {
+ const tabs = this.tabs.filter((item) => ids.includes(item.id));
+
+ tabs.forEach((tab) => {
+ tab.update(payload);
+ });
+ }
+
+ public resetEnvironment(environmentId: string) {
+ const tabs = this.tabs.filter(
+ (tab) => isGrpcRequestTab(tab) && tab.environmentId === environmentId
+ ) as GrpcRequestTab[];
+
+ tabs.forEach((tab) => {
+ tab.update({ environmentId: undefined });
+ });
+ }
+
+ public getTabs() {
+ return this.tabs;
+ }
+
+ public getActiveTab() {
+ return this.tabs.find((item) => item.active);
+ }
+
+ public activateTab(id: string) {
+ const currentActiveTab = this.getActiveTab();
+
+ if (currentActiveTab) {
+ currentActiveTab.active = false;
+ }
+
+ const tab = this.tabs.find((item) => item.id === id);
+
+ if (tab) {
+ tab.update({ active: true });
+ }
+ }
+
+ public closeAllTabs() {
+ this.tabs = [];
+ }
+
+ public closeActiveTab() {
+ const activeTab = this.getActiveTab();
+
+ if (activeTab) {
+ this.closeTab(activeTab.id);
+ }
+ }
+
+ public closeTab(id: string) {
+ const closedTabIndex = this.tabs.findIndex((tab) => tab.id === id);
+
+ if (this.getActiveTab()?.id === id) {
+ this.activateTab(this.tabs[closedTabIndex + 1]?.id || this.tabs[closedTabIndex - 1]?.id);
+ }
+
+ this.tabs.splice(closedTabIndex, 1);
+
+ this.updateTabsOrder();
+ }
+
+ public moveTab(currentId: string, overId: string) {
+ const oldIndex = this.tabs.findIndex((item) => item.id === currentId);
+ const newIndex = this.tabs.findIndex((item) => item.id === overId);
+
+ this.tabs = arrayMove(this.tabs, oldIndex, newIndex);
+
+ this.updateTabsOrder();
+ }
+
+ private updateTabsOrder() {
+ this.tabs.forEach((tab, index) => {
+ tab.update({ order: index + 1 });
+ });
+ }
+}
diff --git a/src/core/entities/tls-presets/index.ts b/src/core/entities/tls-presets/index.ts
new file mode 100644
index 00000000..bb474e9d
--- /dev/null
+++ b/src/core/entities/tls-presets/index.ts
@@ -0,0 +1,2 @@
+export * from './tls-preset.entity';
+export * from './interfaces';
diff --git a/src/core/entities/tls-presets/interfaces/index.ts b/src/core/entities/tls-presets/interfaces/index.ts
new file mode 100644
index 00000000..017e0d11
--- /dev/null
+++ b/src/core/entities/tls-presets/interfaces/index.ts
@@ -0,0 +1 @@
+export * from './tls-preset.interface';
diff --git a/src/core/entities/tls-presets/interfaces/tls-preset.interface.ts b/src/core/entities/tls-presets/interfaces/tls-preset.interface.ts
new file mode 100644
index 00000000..250549ea
--- /dev/null
+++ b/src/core/entities/tls-presets/interfaces/tls-preset.interface.ts
@@ -0,0 +1,13 @@
+import { GrpcChannelOptions, GrpcTlsConfig, GrpcTlsType } from '@getezy/grpc-client';
+
+export interface ITlsPreset {
+ id: string;
+
+ name: string;
+
+ system: boolean;
+
+ tls: GrpcTlsConfig;
+
+ channelOptions?: GrpcChannelOptions;
+}
diff --git a/src/core/entities/tls-presets/tls-preset.entity.ts b/src/core/entities/tls-presets/tls-preset.entity.ts
new file mode 100644
index 00000000..83878163
--- /dev/null
+++ b/src/core/entities/tls-presets/tls-preset.entity.ts
@@ -0,0 +1,37 @@
+import { AutoMap } from '@automapper/classes';
+import type { GrpcChannelOptions, GrpcTlsConfig } from '@getezy/grpc-client';
+import { GrpcTlsType } from '@getezy/grpc-client';
+import { v4 as uuid } from 'uuid';
+
+import { ITlsPreset } from './interfaces';
+
+export class TlsPreset implements ITlsPreset {
+ @AutoMap()
+ id: string;
+
+ @AutoMap()
+ name: string;
+
+ @AutoMap()
+ system: boolean;
+
+ tls: GrpcTlsConfig;
+
+ @AutoMap()
+ channelOptions?: GrpcChannelOptions;
+
+ static create(data: Omit, 'id'>) {
+ return new TlsPreset({
+ id: uuid(),
+ ...data,
+ });
+ }
+
+ constructor({ id, name, system, tls, channelOptions }: ITlsPreset) {
+ this.id = id;
+ this.name = name;
+ this.system = system;
+ this.tls = tls;
+ this.channelOptions = channelOptions;
+ }
+}
diff --git a/src/core/index.ts b/src/core/index.ts
index 42681098..3c7ba8bb 100644
--- a/src/core/index.ts
+++ b/src/core/index.ts
@@ -1,2 +1,4 @@
export * from './clients';
export * from './protobuf';
+export * from './entities';
+export * from './protocols';
diff --git a/src/core/protobuf/__tests__/protobuf-loader.spec.ts b/src/core/protobuf/__tests__/protobuf-loader.spec.ts
deleted file mode 100644
index 482214aa..00000000
--- a/src/core/protobuf/__tests__/protobuf-loader.spec.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-import { join } from 'path';
-
-import { GrpcMethodType } from '../interfaces';
-import { ProtobufLoader } from '../protobuf-loader';
-
-describe('ProtobufLoader', () => {
- describe('ProtobufLoader:loadFromFile', () => {
- it('should load simple proto', async () => {
- const ast = await ProtobufLoader.loadFromFile({
- path: join(__dirname, '../../__tests__/fixtures/proto/simple.proto'),
- });
-
- expect(ast).toBeDefined();
- expect(ast['simple_package.v1.SimpleService']).toBeDefined();
- });
-
- it('should load proto that does not exist', async () => {
- expect(() =>
- ProtobufLoader.loadFromFile({
- path: join(__dirname, '../../__tests__/fixtures/proto/another.proto'),
- })
- ).rejects.toThrow();
- });
- });
-
- describe('ProtobufLoader:parse', () => {
- it('should parse basic proto without package definition', async () => {
- const ast = await ProtobufLoader.loadFromFile({
- path: join(__dirname, '../../__tests__/fixtures/proto/basic.proto'),
- });
-
- const packages = ProtobufLoader.parse(ast);
-
- expect(packages).toEqual([
- {
- name: 'BasicService',
- methods: [
- {
- name: 'BasicRequest',
- type: GrpcMethodType.UNARY,
- },
- ],
- },
- ]);
- });
-
- it('should parse simple proto', async () => {
- const ast = await ProtobufLoader.loadFromFile({
- path: join(__dirname, '../../__tests__/fixtures/proto/simple.proto'),
- });
-
- const packages = ProtobufLoader.parse(ast);
-
- expect(packages).toEqual([
- {
- name: 'simple_package.v1.SimpleService',
- methods: [
- {
- name: 'SimpleUnaryRequest',
- type: GrpcMethodType.UNARY,
- },
- {
- name: 'SimpleClientStreamRequest',
- type: GrpcMethodType.CLIENT_STREAMING,
- },
- {
- name: 'SimpleServerStreamRequest',
- type: GrpcMethodType.SERVER_STREAMING,
- },
- {
- name: 'SimpleBidirectionalStreamRequest',
- type: GrpcMethodType.BIDIRECTIONAL_STREAMING,
- },
- ],
- },
- ]);
- });
- });
-});
diff --git a/src/core/protobuf/interfaces/protobuf-loader.interface.ts b/src/core/protobuf/interfaces/protobuf-loader.interface.ts
index 48125efd..a4468e92 100644
--- a/src/core/protobuf/interfaces/protobuf-loader.interface.ts
+++ b/src/core/protobuf/interfaces/protobuf-loader.interface.ts
@@ -5,14 +5,14 @@ export enum GrpcMethodType {
BIDIRECTIONAL_STREAMING = 'bidirectional-streaming',
}
-export type GrpcMethodInfo = {
+export type GrpcMethodDefinition = {
name: string;
type: GrpcMethodType;
};
-export type GrpcServiceInfo = {
+export type GrpcServiceDefinition = {
name: string;
- methods?: GrpcMethodInfo[];
+ methods: GrpcMethodDefinition[];
};
export type GrpcOptions = {
diff --git a/src/core/protobuf/protobuf-loader.ts b/src/core/protobuf/protobuf-loader.ts
index 4f710dc5..0bc70fbb 100644
--- a/src/core/protobuf/protobuf-loader.ts
+++ b/src/core/protobuf/protobuf-loader.ts
@@ -6,7 +6,12 @@ import type {
} from '@grpc/proto-loader';
import * as protoloader from '@grpc/proto-loader';
-import { GrpcMethodInfo, GrpcMethodType, GrpcOptions, GrpcServiceInfo } from './interfaces';
+import {
+ GrpcMethodDefinition,
+ GrpcMethodType,
+ GrpcOptions,
+ GrpcServiceDefinition,
+} from './interfaces';
function instanceOfProtobufTypeDefinition(object: any): object is ProtobufTypeDefinition {
return 'type' in object;
@@ -28,8 +33,8 @@ export class ProtobufLoader {
return ast;
}
- static parse(ast: PackageDefinition): GrpcServiceInfo[] {
- const services: GrpcServiceInfo[] = [];
+ static parse(ast: PackageDefinition): GrpcServiceDefinition[] {
+ const services: GrpcServiceDefinition[] = [];
const packages = Object.keys(ast);
for (let i = 0; i < packages.length; i++) {
@@ -45,20 +50,16 @@ export class ProtobufLoader {
return services;
}
- private static parseService(name: string, astService: ServiceDefinition): GrpcServiceInfo {
- const parsedService: GrpcServiceInfo = {
- name,
- };
-
+ private static parseService(name: string, astService: ServiceDefinition): GrpcServiceDefinition {
const astMethods = Object.keys(astService);
- const methods: GrpcMethodInfo[] = [];
+ const methods: GrpcMethodDefinition[] = [];
for (let i = 0; i < astMethods.length; i++) {
const astItem = astService[astMethods[i]];
if (instanceOfMethodDefinition(astItem)) {
- const method: GrpcMethodInfo = {
+ const method: GrpcMethodDefinition = {
name: astMethods[i],
type: this.getMethodType(astItem),
};
@@ -68,7 +69,7 @@ export class ProtobufLoader {
}
return {
- ...parsedService,
+ name,
methods,
};
}
diff --git a/src/core/protocols/grpc/grpc-protocol-type.enum.ts b/src/core/protocols/grpc/grpc-protocol-type.enum.ts
new file mode 100644
index 00000000..ac5f9ae2
--- /dev/null
+++ b/src/core/protocols/grpc/grpc-protocol-type.enum.ts
@@ -0,0 +1,4 @@
+export enum GrpcProtocolType {
+ Grpc = 'grpc',
+ GrpcWeb = 'grpc-web',
+}
diff --git a/src/core/protocols/grpc/index.ts b/src/core/protocols/grpc/index.ts
new file mode 100644
index 00000000..daa7ab2e
--- /dev/null
+++ b/src/core/protocols/grpc/index.ts
@@ -0,0 +1 @@
+export * from './grpc-protocol-type.enum';
diff --git a/src/app/hooks/protocols/index.ts b/src/core/protocols/index.ts
similarity index 100%
rename from src/app/hooks/protocols/index.ts
rename to src/core/protocols/index.ts
diff --git a/src/core/typings.ts b/src/core/types.d.ts
similarity index 71%
rename from src/core/typings.ts
rename to src/core/types.d.ts
index 26458b8a..cb4af7fd 100644
--- a/src/core/typings.ts
+++ b/src/core/types.d.ts
@@ -1,3 +1,5 @@
-export * from './protobuf/interfaces';
export * from './clients/grpc/interfaces';
export * from './clients/grpc/grpc-web-client/interfaces';
+export * from './protobuf/interfaces';
+export * from './entities';
+export * from './protocols';
diff --git a/src/main/clients/grpc-client/preload/handlers.ts b/src/main/clients/grpc-client/preload/handlers.ts
index 4278045d..93c0086e 100644
--- a/src/main/clients/grpc-client/preload/handlers.ts
+++ b/src/main/clients/grpc-client/preload/handlers.ts
@@ -8,7 +8,7 @@ export type OnErrorCallback = (error: ServerErrorResponse) => void;
export type OnEndCallback = () => void;
export function wrapHandler(streamId: string, callback: (...callbackArgs: any[]) => void) {
- return function wrappedHandler(event: IpcRendererEvent, id: string, ...args: any[]) {
+ return function wrappedHandler(_event: IpcRendererEvent, id: string, ...args: any[]) {
if (streamId === id) {
callback(...args);
}
diff --git a/src/main/clients/grpc-client/subscribers/bidirectional-streaming.subscriber.ts b/src/main/clients/grpc-client/subscribers/bidirectional-streaming.subscriber.ts
index 06933a59..ccebf012 100644
--- a/src/main/clients/grpc-client/subscribers/bidirectional-streaming.subscriber.ts
+++ b/src/main/clients/grpc-client/subscribers/bidirectional-streaming.subscriber.ts
@@ -1,6 +1,6 @@
import { ClientDuplexStream, MetadataValue } from '@grpc/grpc-js';
import { BrowserWindow, IpcMain } from 'electron';
-import { nanoid } from 'nanoid';
+import * as uuid from 'uuid';
import { GrpcClient, GrpcClientRequestOptions, GrpcOptions, ProtobufLoader } from '@core';
@@ -77,7 +77,7 @@ export class GrpcClientBidirectionalSubscriber {
private registerBidirectionalStreamingCall(
call: ClientDuplexStream, Record>
): string {
- const id = nanoid();
+ const id = uuid.v4();
call.on('data', (data) => {
this.mainWindow.webContents.send(GrpcClientBidirectionalStreamingChannel.DATA, id, data);
diff --git a/src/main/clients/grpc-client/subscribers/client-streaming.subscriber.ts b/src/main/clients/grpc-client/subscribers/client-streaming.subscriber.ts
index 19486a11..2823ad36 100644
--- a/src/main/clients/grpc-client/subscribers/client-streaming.subscriber.ts
+++ b/src/main/clients/grpc-client/subscribers/client-streaming.subscriber.ts
@@ -1,6 +1,6 @@
import { ClientWritableStream, MetadataValue } from '@grpc/grpc-js';
import { BrowserWindow, IpcMain } from 'electron';
-import { nanoid } from 'nanoid';
+import * as uuid from 'uuid';
import { GrpcClient, GrpcClientRequestOptions, GrpcOptions, ProtobufLoader } from '@core';
@@ -68,7 +68,7 @@ export class GrpcClientClientStreamingSubscriber {
}
private registerClientStreamingCall(call: ClientWritableStream>): string {
- const id = nanoid();
+ const id = uuid.v4();
call.on('data', (data) => {
this.mainWindow.webContents.send(GrpcClientClientStreamingChannel.DATA, id, data);
diff --git a/src/main/clients/grpc-client/subscribers/index.ts b/src/main/clients/grpc-client/subscribers/index.ts
index 79801a9d..b19ee13a 100644
--- a/src/main/clients/grpc-client/subscribers/index.ts
+++ b/src/main/clients/grpc-client/subscribers/index.ts
@@ -9,7 +9,7 @@ export const registerGrpcClientSubscribers = (mainWindow: BrowserWindow) => {
const bidirectional = new GrpcClientBidirectionalSubscriber(mainWindow, ipcMain);
const client = new GrpcClientClientStreamingSubscriber(mainWindow, ipcMain);
const server = new GrpcClientServerStreamingSubscriber(mainWindow, ipcMain);
- const unary = new GrpcClientUnarySubscriber(mainWindow, ipcMain);
+ const unary = new GrpcClientUnarySubscriber(ipcMain);
bidirectional.registerBidirectionalStreamingHandlers();
client.registerClientStreamingHandlers();
diff --git a/src/main/clients/grpc-client/subscribers/server-streaming.subscriber.ts b/src/main/clients/grpc-client/subscribers/server-streaming.subscriber.ts
index 9b53ca01..f6a32a99 100644
--- a/src/main/clients/grpc-client/subscribers/server-streaming.subscriber.ts
+++ b/src/main/clients/grpc-client/subscribers/server-streaming.subscriber.ts
@@ -1,6 +1,6 @@
import { ClientReadableStream, MetadataValue } from '@grpc/grpc-js';
import { BrowserWindow, IpcMain } from 'electron';
-import { nanoid } from 'nanoid';
+import * as uuid from 'uuid';
import { GrpcClient, GrpcClientRequestOptions, GrpcOptions, ProtobufLoader } from '@core';
@@ -51,7 +51,7 @@ export class GrpcClientServerStreamingSubscriber {
}
private registerServerStreamingCall(call: ClientReadableStream>): string {
- const id = nanoid();
+ const id = uuid.v4();
call.on('data', (data) => {
this.mainWindow.webContents.send(GrpcClientServerStreamingChannel.DATA, id, data);
diff --git a/src/main/clients/grpc-client/subscribers/unary.subscriber.ts b/src/main/clients/grpc-client/subscribers/unary.subscriber.ts
index e846a2c7..d2472e8b 100644
--- a/src/main/clients/grpc-client/subscribers/unary.subscriber.ts
+++ b/src/main/clients/grpc-client/subscribers/unary.subscriber.ts
@@ -1,12 +1,12 @@
import { MetadataValue } from '@grpc/grpc-js';
-import { BrowserWindow, IpcMain } from 'electron';
+import { IpcMain } from 'electron';
import { GrpcClient, GrpcClientRequestOptions, GrpcOptions, ProtobufLoader } from '@core';
import { GrpcClientChannel } from '../constants';
export class GrpcClientUnarySubscriber {
- constructor(private readonly mainWindow: BrowserWindow, private readonly ipcMain: IpcMain) {}
+ constructor(private readonly ipcMain: IpcMain) {}
public static unregisterUnaryCallHandlers(ipcMain: IpcMain) {
ipcMain.removeHandler(GrpcClientChannel.INVOKE_UNARY_REQUEST);
diff --git a/src/main/clients/grpc-web-client/preload/handlers.ts b/src/main/clients/grpc-web-client/preload/handlers.ts
index ec9901e1..feb88919 100644
--- a/src/main/clients/grpc-web-client/preload/handlers.ts
+++ b/src/main/clients/grpc-web-client/preload/handlers.ts
@@ -9,7 +9,7 @@ export type OnErrorCallback = (error: GrpcWebError) => void;
export type OnEndCallback = () => void;
export function wrapHandler(streamId: string, callback: (...callbackArgs: any[]) => void) {
- return function wrappedHandler(event: IpcRendererEvent, id: string, ...args: any[]) {
+ return function wrappedHandler(_event: IpcRendererEvent, id: string, ...args: any[]) {
if (streamId === id) {
callback(...args);
}
diff --git a/src/main/clients/grpc-web-client/subscribers/index.ts b/src/main/clients/grpc-web-client/subscribers/index.ts
index 71939099..ed4a691a 100644
--- a/src/main/clients/grpc-web-client/subscribers/index.ts
+++ b/src/main/clients/grpc-web-client/subscribers/index.ts
@@ -5,7 +5,7 @@ import { GrpcWebClientUnarySubscriber } from './unary.subscriber';
export const registerGrpcWebClientSubscribers = (mainWindow: BrowserWindow) => {
const server = new GrpcWebClientServerStreamingSubscriber(mainWindow, ipcMain);
- const unary = new GrpcWebClientUnarySubscriber(mainWindow, ipcMain);
+ const unary = new GrpcWebClientUnarySubscriber(ipcMain);
server.registerServerStreamingHandlers();
unary.registerUnaryCallHandlers();
diff --git a/src/main/clients/grpc-web-client/subscribers/server-streaming.subscriber.ts b/src/main/clients/grpc-web-client/subscribers/server-streaming.subscriber.ts
index 31d476db..ac2288ef 100644
--- a/src/main/clients/grpc-web-client/subscribers/server-streaming.subscriber.ts
+++ b/src/main/clients/grpc-web-client/subscribers/server-streaming.subscriber.ts
@@ -1,5 +1,5 @@
import { BrowserWindow, IpcMain } from 'electron';
-import { nanoid } from 'nanoid';
+import * as uuid from 'uuid';
import {
GrpcClientRequestOptions,
@@ -60,7 +60,7 @@ export class GrpcWebClientServerStreamingSubscriber {
}
private registerServerStreamingCall(call: GrpcWebCallStream): string {
- const id = nanoid();
+ const id = uuid.v4();
call.on('message', (message) => {
this.mainWindow.webContents.send(GrpcWebClientServerStreamingChannel.DATA, id, message);
diff --git a/src/main/clients/grpc-web-client/subscribers/unary.subscriber.ts b/src/main/clients/grpc-web-client/subscribers/unary.subscriber.ts
index 1d71f502..0bd9a9ed 100644
--- a/src/main/clients/grpc-web-client/subscribers/unary.subscriber.ts
+++ b/src/main/clients/grpc-web-client/subscribers/unary.subscriber.ts
@@ -1,4 +1,4 @@
-import { BrowserWindow, IpcMain } from 'electron';
+import { IpcMain } from 'electron';
import {
GrpcClientRequestOptions,
@@ -11,7 +11,7 @@ import {
import { GrpcWebClientChannel } from '../constants';
export class GrpcWebClientUnarySubscriber {
- constructor(private readonly mainWindow: BrowserWindow, private readonly ipcMain: IpcMain) {}
+ constructor(private readonly ipcMain: IpcMain) {}
public static unregisterUnaryCallHandlers(ipcMain: IpcMain) {
ipcMain.removeHandler(GrpcWebClientChannel.INVOKE_UNARY_REQUEST);
diff --git a/src/main/database/common/constants.ts b/src/main/database/common/constants.ts
new file mode 100644
index 00000000..4a685303
--- /dev/null
+++ b/src/main/database/common/constants.ts
@@ -0,0 +1,8 @@
+export enum DatabaseChannel {
+ FIND = 'database:find',
+ FIND_ONE = 'database:find-one',
+ FIND_ONE_OR_FAIL = 'database:find-one-or-fail',
+ UPSERT = 'database:upsert',
+ UPSERT_MANY = 'database:upsert-many',
+ DELETE = 'database:delete',
+}
diff --git a/src/main/database/common/database-path.ts b/src/main/database/common/database-path.ts
new file mode 100644
index 00000000..2d33635e
--- /dev/null
+++ b/src/main/database/common/database-path.ts
@@ -0,0 +1,4 @@
+import { app } from 'electron';
+import path from 'path';
+
+export const DATABSE_PATH = path.join(app.getPath('userData'), 'ezy.db');
diff --git a/src/main/database/common/index.ts b/src/main/database/common/index.ts
new file mode 100644
index 00000000..7474dfbe
--- /dev/null
+++ b/src/main/database/common/index.ts
@@ -0,0 +1,4 @@
+export * from './constants';
+export * from './preload.factory';
+export * from './subscriber.factory';
+export * from './mapper';
diff --git a/src/main/database/common/interaces/index.ts b/src/main/database/common/interaces/index.ts
new file mode 100644
index 00000000..86b9acf7
--- /dev/null
+++ b/src/main/database/common/interaces/index.ts
@@ -0,0 +1 @@
+export * from './type.interface';
diff --git a/src/main/database/common/interaces/type.interface.ts b/src/main/database/common/interaces/type.interface.ts
new file mode 100644
index 00000000..2e148dad
--- /dev/null
+++ b/src/main/database/common/interaces/type.interface.ts
@@ -0,0 +1,3 @@
+export interface Type extends Function {
+ new (...args: any[]): T;
+}
diff --git a/src/main/database/common/knex.ts b/src/main/database/common/knex.ts
new file mode 100644
index 00000000..4a7698ec
--- /dev/null
+++ b/src/main/database/common/knex.ts
@@ -0,0 +1,16 @@
+import Knex from 'knex';
+import path from 'path';
+
+import { DATABSE_PATH } from './database-path';
+
+export const knex = Knex({
+ client: 'sqlite',
+ connection: {
+ filename: DATABSE_PATH,
+ },
+ useNullAsDefault: true,
+ migrations: {
+ tableName: 'migrations',
+ directory: path.join(__dirname, './migrations'),
+ },
+});
diff --git a/src/main/database/common/mapper.ts b/src/main/database/common/mapper.ts
new file mode 100644
index 00000000..6581068f
--- /dev/null
+++ b/src/main/database/common/mapper.ts
@@ -0,0 +1,6 @@
+import { createMapper } from '@automapper/core';
+import { mikro } from '@automapper/mikro';
+
+export const mapper = createMapper({
+ strategyInitializer: mikro(),
+});
diff --git a/src/main/database/common/preload.factory.ts b/src/main/database/common/preload.factory.ts
new file mode 100644
index 00000000..4709cd7f
--- /dev/null
+++ b/src/main/database/common/preload.factory.ts
@@ -0,0 +1,56 @@
+import { EntityData, FilterQuery } from '@mikro-orm/core';
+import { ipcRenderer } from 'electron';
+
+import { DatabaseChannel } from './constants';
+import { Type } from './interaces';
+
+export type CustomPreload = {
+ [K: string]: (...args: any[]) => Promise | Promise;
+};
+
+export class PreloadFactory {
+ static create(
+ entity: Type,
+ customPreload?: CustomPreload
+ ) {
+ return {
+ find(where: FilterQuery): Promise {
+ return ipcRenderer.invoke(`${DatabaseChannel.FIND}:${entity.name.toLowerCase()}`, where);
+ },
+
+ findOne(where: FilterQuery): Promise {
+ return ipcRenderer.invoke(
+ `${DatabaseChannel.FIND_ONE}:${entity.name.toLowerCase()}}`,
+ where
+ );
+ },
+
+ findOneOrFail(where: FilterQuery): Promise {
+ return ipcRenderer.invoke(
+ `${DatabaseChannel.FIND_ONE_OR_FAIL}:${entity.name.toLowerCase()}`,
+ where
+ );
+ },
+
+ upsert(payload: EntityData): Promise {
+ return ipcRenderer.invoke(
+ `${DatabaseChannel.UPSERT}:${entity.name.toLowerCase()}`,
+ payload
+ );
+ },
+
+ upsertMany(payload: EntityData[]): Promise {
+ return ipcRenderer.invoke(
+ `${DatabaseChannel.UPSERT_MANY}:${entity.name.toLowerCase()}`,
+ payload
+ );
+ },
+
+ delete(where: FilterQuery): Promise {
+ return ipcRenderer.invoke(`${DatabaseChannel.DELETE}:${entity.name.toLowerCase()}`, where);
+ },
+
+ ...customPreload,
+ };
+ }
+}
diff --git a/src/main/database/common/subscriber.factory.ts b/src/main/database/common/subscriber.factory.ts
new file mode 100644
index 00000000..aa8b38bb
--- /dev/null
+++ b/src/main/database/common/subscriber.factory.ts
@@ -0,0 +1,95 @@
+import { Mapper } from '@automapper/core';
+import { EntityData, FilterQuery, MikroORM } from '@mikro-orm/core';
+import { SqliteDriver } from '@mikro-orm/sqlite';
+import { ipcMain } from 'electron';
+
+import { DatabaseChannel } from './constants';
+import { Type } from './interaces';
+import { mapper } from './mapper';
+
+export type CustomSubscriber = {
+ channel: string;
+ handler: (
+ orm: MikroORM,
+ mapper: Mapper,
+ entity: Type,
+ view: Type
+ ) => (...args: any[]) => Promise;
+};
+
+export class SubscriberFactory {
+ static create(
+ orm: MikroORM,
+ entity: Type,
+ view: Type,
+ customSubscribers: CustomSubscriber[] = []
+ ) {
+ customSubscribers.forEach((subscriber) => {
+ ipcMain.handle(subscriber.channel, subscriber.handler(orm, mapper, entity, view));
+ });
+
+ ipcMain.handle(
+ `${DatabaseChannel.FIND}:${entity.name.toLowerCase()}`,
+ async (_event, where: FilterQuery): Promise => {
+ const em = orm.em.fork();
+
+ const data = await em.find(entity, where);
+
+ return mapper.mapArray(data, entity, view);
+ }
+ );
+
+ ipcMain.handle(
+ `${DatabaseChannel.FIND_ONE}:${entity.name.toLowerCase()}`,
+ async (_event, where: FilterQuery): Promise => {
+ const em = orm.em.fork();
+
+ const data = await em.findOne(entity, where);
+
+ return mapper.map(data, entity, view);
+ }
+ );
+
+ ipcMain.handle(
+ `${DatabaseChannel.FIND_ONE_OR_FAIL}:${entity.name.toLowerCase()}`,
+ async (_event, where: FilterQuery): Promise => {
+ const em = orm.em.fork();
+
+ const data = await em.findOneOrFail(entity, where);
+
+ return mapper.map(data, entity, view);
+ }
+ );
+
+ ipcMain.handle(
+ `${DatabaseChannel.UPSERT}:${entity.name.toLowerCase()}`,
+ async (_event, payload: EntityData): Promise => {
+ const em = orm.em.fork();
+
+ const data = await em.upsert(entity, payload);
+
+ return mapper.map(data, entity, view);
+ }
+ );
+
+ ipcMain.handle(
+ `${DatabaseChannel.UPSERT_MANY}:${entity.name.toLowerCase()}`,
+ async (_event, payload: EntityData[]): Promise => {
+ const em = orm.em.fork();
+
+ const data = await em.upsertMany(entity, payload);
+
+ return mapper.mapArray(data, entity, view);
+ }
+ );
+
+ ipcMain.handle(
+ `${DatabaseChannel.DELETE}:${entity.name.toLowerCase()}`,
+ async (_event, where: FilterQuery): Promise => {
+ const em = orm.em.fork();
+
+ await em.nativeDelete(entity, where);
+ }
+ );
+ }
+}
diff --git a/src/main/database/entities/environments/environment.entity.ts b/src/main/database/entities/environments/environment.entity.ts
new file mode 100644
index 00000000..f5f94279
--- /dev/null
+++ b/src/main/database/entities/environments/environment.entity.ts
@@ -0,0 +1,26 @@
+import { AutoMap } from '@automapper/classes';
+import { Entity, EntityRepositoryType, PrimaryKey, Property } from '@mikro-orm/core';
+
+// eslint-disable-next-line import/no-cycle
+import { EnvironmentsRepository } from './environments.repository';
+
+@Entity({ tableName: 'environments', customRepository: () => EnvironmentsRepository })
+export class Environment {
+ [EntityRepositoryType]?: EnvironmentsRepository;
+
+ @PrimaryKey()
+ @AutoMap()
+ id!: string;
+
+ @Property()
+ @AutoMap()
+ label!: string;
+
+ @Property()
+ @AutoMap()
+ url!: string;
+
+ @Property()
+ @AutoMap()
+ color!: string;
+}
diff --git a/src/main/database/entities/environments/environment.mapper.ts b/src/main/database/entities/environments/environment.mapper.ts
new file mode 100644
index 00000000..2dbcbef9
--- /dev/null
+++ b/src/main/database/entities/environments/environment.mapper.ts
@@ -0,0 +1,15 @@
+import { constructUsing, createMap } from '@automapper/core';
+
+import { Environment as EnvironmentView } from '@core';
+
+import { mapper } from '../../common';
+import { Environment } from './environment.entity';
+
+export function createEnvironmentMappings() {
+ createMap(
+ mapper,
+ Environment,
+ EnvironmentView,
+ constructUsing((sourceObject) => new EnvironmentView(sourceObject))
+ );
+}
diff --git a/src/main/database/entities/environments/environments.repository.ts b/src/main/database/entities/environments/environments.repository.ts
new file mode 100644
index 00000000..1ef38a67
--- /dev/null
+++ b/src/main/database/entities/environments/environments.repository.ts
@@ -0,0 +1,6 @@
+import { EntityRepository } from '@mikro-orm/sqlite';
+
+// eslint-disable-next-line import/no-cycle
+import { Environment } from './environment.entity';
+
+export class EnvironmentsRepository extends EntityRepository {}
diff --git a/src/main/database/entities/environments/index.ts b/src/main/database/entities/environments/index.ts
new file mode 100644
index 00000000..6a152762
--- /dev/null
+++ b/src/main/database/entities/environments/index.ts
@@ -0,0 +1,3 @@
+export * from './environment.entity';
+export * from './environments.repository';
+export * from './environment.mapper';
diff --git a/src/main/database/entities/index.ts b/src/main/database/entities/index.ts
new file mode 100644
index 00000000..a91e9eb6
--- /dev/null
+++ b/src/main/database/entities/index.ts
@@ -0,0 +1,4 @@
+export * from './settings';
+export * from './environments';
+export * from './tls-presets';
+export * from './tabs';
diff --git a/src/main/database/entities/settings/index.ts b/src/main/database/entities/settings/index.ts
new file mode 100644
index 00000000..1108d995
--- /dev/null
+++ b/src/main/database/entities/settings/index.ts
@@ -0,0 +1,3 @@
+export * from './setting.entity';
+export * from './settings.repository';
+export * from './setting.mapper';
diff --git a/src/main/database/entities/settings/setting.entity.ts b/src/main/database/entities/settings/setting.entity.ts
new file mode 100644
index 00000000..25df0bea
--- /dev/null
+++ b/src/main/database/entities/settings/setting.entity.ts
@@ -0,0 +1,67 @@
+/* eslint-disable max-classes-per-file */
+
+import { AutoMap } from '@automapper/classes';
+import { Entity, EntityRepositoryType, Enum, JsonType, Property } from '@mikro-orm/core';
+
+import { Alignment, Language, MenuOptions, SettingKey, Theme } from '@core';
+
+// eslint-disable-next-line import/no-cycle
+import { SettingsRepository } from './settings.repository';
+
+export class ThemeValue {
+ @Enum(() => Theme)
+ theme: Theme;
+
+ constructor(theme: Theme) {
+ this.theme = theme;
+ }
+}
+
+export class AlignmentValue {
+ @Enum(() => Alignment)
+ alignment: Alignment;
+
+ constructor(alignment: Alignment) {
+ this.alignment = alignment;
+ }
+}
+
+export class LanguageValue {
+ @Enum(() => Language)
+ language: Language;
+
+ constructor(language: Language) {
+ this.language = language;
+ }
+}
+
+export class MenuOptionsValue {
+ @Property()
+ collapsed: boolean;
+
+ constructor({ collapsed }: MenuOptions) {
+ this.collapsed = collapsed;
+ }
+}
+
+export type SettingValue = Key extends SettingKey.ALIGNMENT
+ ? AlignmentValue
+ : Key extends SettingKey.LANGUAGE
+ ? LanguageValue
+ : Key extends SettingKey.MENU
+ ? MenuOptionsValue
+ : Key extends SettingKey.THEME
+ ? ThemeValue
+ : never;
+
+@Entity({ tableName: 'settings', customRepository: () => SettingsRepository })
+export class Setting {
+ [EntityRepositoryType]?: SettingsRepository;
+
+ @Enum({ type: 'string', items: () => SettingKey, primary: true })
+ @AutoMap(() => String)
+ key!: Key;
+
+ @Property({ type: JsonType })
+ value!: SettingValue;
+}
diff --git a/src/main/database/entities/settings/setting.mapper.ts b/src/main/database/entities/settings/setting.mapper.ts
new file mode 100644
index 00000000..4f4a175e
--- /dev/null
+++ b/src/main/database/entities/settings/setting.mapper.ts
@@ -0,0 +1,44 @@
+import { constructUsing, createMap, forMember, mapFrom } from '@automapper/core';
+
+import { Setting as SettingView, SettingKey } from '@core';
+
+import { mapper } from '../../common';
+import { Setting } from './setting.entity';
+import {
+ isAlignmentSetting,
+ isLanguageSetting,
+ isMenuOptionsSetting,
+ isThemeSetting,
+} from './settings.guards';
+
+function getValue(source: Setting) {
+ if (isAlignmentSetting(source)) {
+ return source.value.alignment;
+ }
+ if (isLanguageSetting(source)) {
+ return source.value.language;
+ }
+ if (isMenuOptionsSetting(source)) {
+ return source.value;
+ }
+ if (isThemeSetting(source)) {
+ return source.value.theme;
+ }
+
+ throw new Error('Error while mapping setting value');
+}
+
+export function createSettingMappings() {
+ createMap(
+ mapper,
+ Setting,
+ SettingView,
+ forMember(
+ (destination) => destination.value,
+ mapFrom((source) => getValue(source))
+ ),
+ constructUsing(
+ (sourceObject) => new SettingView({ key: sourceObject.key, value: getValue(sourceObject) })
+ )
+ );
+}
diff --git a/src/main/database/entities/settings/settings.guards.ts b/src/main/database/entities/settings/settings.guards.ts
new file mode 100644
index 00000000..727d1c9e
--- /dev/null
+++ b/src/main/database/entities/settings/settings.guards.ts
@@ -0,0 +1,19 @@
+import { SettingKey } from '@core';
+
+import { Setting } from './setting.entity';
+
+export function isAlignmentSetting(value: any): value is Setting {
+ return value?.key === SettingKey.ALIGNMENT;
+}
+
+export function isLanguageSetting(value: any): value is Setting {
+ return value?.key === SettingKey.LANGUAGE;
+}
+
+export function isMenuOptionsSetting(value: any): value is Setting {
+ return value?.key === SettingKey.MENU;
+}
+
+export function isThemeSetting(value: any): value is Setting {
+ return value?.key === SettingKey.THEME;
+}
diff --git a/src/main/database/entities/settings/settings.repository.ts b/src/main/database/entities/settings/settings.repository.ts
new file mode 100644
index 00000000..80171ac0
--- /dev/null
+++ b/src/main/database/entities/settings/settings.repository.ts
@@ -0,0 +1,6 @@
+import { EntityRepository } from '@mikro-orm/sqlite';
+
+// eslint-disable-next-line import/no-cycle
+import { Setting } from './setting.entity';
+
+export class SettingsRepository extends EntityRepository {}
diff --git a/src/main/database/entities/tabs/grpc-request/grpc-request-tab.entity.ts b/src/main/database/entities/tabs/grpc-request/grpc-request-tab.entity.ts
new file mode 100644
index 00000000..52202a23
--- /dev/null
+++ b/src/main/database/entities/tabs/grpc-request/grpc-request-tab.entity.ts
@@ -0,0 +1,18 @@
+import { AutoMap } from '@automapper/classes';
+import { Entity, Enum, PrimaryKey, Property } from '@mikro-orm/core';
+
+import { GrpcProtocolType } from '@core';
+
+@Entity({ tableName: 'grpc-request-tabs' })
+export class GrpcRequestTab {
+ @PrimaryKey()
+ tab_id!: string;
+
+ @Enum({ type: 'string', items: () => GrpcProtocolType })
+ @AutoMap()
+ protocol!: GrpcProtocolType;
+
+ @Property()
+ @AutoMap()
+ url!: string;
+}
diff --git a/src/main/database/entities/tabs/grpc-request/index.ts b/src/main/database/entities/tabs/grpc-request/index.ts
new file mode 100644
index 00000000..e8aabb9b
--- /dev/null
+++ b/src/main/database/entities/tabs/grpc-request/index.ts
@@ -0,0 +1 @@
+export * from './grpc-request-tab.entity';
diff --git a/src/main/database/entities/tabs/index.ts b/src/main/database/entities/tabs/index.ts
new file mode 100644
index 00000000..2be09dc2
--- /dev/null
+++ b/src/main/database/entities/tabs/index.ts
@@ -0,0 +1,4 @@
+export * from './tab.entity';
+export * from './tabs.repository';
+export * from './tab.mapper';
+export * from './grpc-request';
diff --git a/src/main/database/entities/tabs/tab.entity.ts b/src/main/database/entities/tabs/tab.entity.ts
new file mode 100644
index 00000000..ba1d846c
--- /dev/null
+++ b/src/main/database/entities/tabs/tab.entity.ts
@@ -0,0 +1,56 @@
+/* eslint-disable max-classes-per-file */
+
+import { AutoMap } from '@automapper/classes';
+import {
+ Embeddable,
+ Embedded,
+ Entity,
+ EntityRepositoryType,
+ Enum,
+ PrimaryKey,
+ Property,
+} from '@mikro-orm/core';
+
+import { GrpcProtocolType, TabType } from '@core';
+
+// eslint-disable-next-line import/no-cycle
+import { TabsRepository } from './tabs.repository';
+
+@Embeddable({ abstract: true, discriminatorColumn: 'type' })
+export abstract class TabData {
+ @Enum({ type: 'string', items: () => TabType })
+ type!: TabType;
+}
+
+@Embeddable({ discriminatorValue: TabType.GrpcRequest })
+export class GrpcRequestTabData extends TabData {
+ @Enum({ type: 'string', items: () => GrpcProtocolType })
+ protocol!: GrpcProtocolType;
+
+ @Property()
+ url?: string;
+}
+
+@Entity({ tableName: 'tabs', customRepository: () => TabsRepository })
+export class Tab {
+ [EntityRepositoryType]?: TabsRepository;
+
+ @PrimaryKey()
+ @AutoMap()
+ id!: string;
+
+ @Property()
+ @AutoMap()
+ title!: string;
+
+ @Property()
+ @AutoMap()
+ active!: boolean;
+
+ @Property()
+ @AutoMap()
+ order!: number;
+
+ @Embedded(() => [GrpcRequestTabData], { object: true })
+ data!: GrpcRequestTabData;
+}
diff --git a/src/main/database/entities/tabs/tab.mapper.ts b/src/main/database/entities/tabs/tab.mapper.ts
new file mode 100644
index 00000000..dea5f32a
--- /dev/null
+++ b/src/main/database/entities/tabs/tab.mapper.ts
@@ -0,0 +1,25 @@
+import { addProfile, constructUsing, createMap, MappingProfile } from '@automapper/core';
+
+import { GrpcRequestTab } from '@core';
+
+import { mapper } from '../../common';
+import { Tab } from './tab.entity';
+
+export function createTabMappings() {
+ const tabProfile: MappingProfile = (m) => {
+ createMap(
+ m,
+ Tab,
+ GrpcRequestTab,
+ constructUsing(
+ ({ data, ...base }) =>
+ new GrpcRequestTab({
+ ...base,
+ ...data,
+ })
+ )
+ );
+ };
+
+ addProfile(mapper, tabProfile);
+}
diff --git a/src/main/database/entities/tabs/tabs.repository.ts b/src/main/database/entities/tabs/tabs.repository.ts
new file mode 100644
index 00000000..c2294ec5
--- /dev/null
+++ b/src/main/database/entities/tabs/tabs.repository.ts
@@ -0,0 +1,6 @@
+import { EntityRepository } from '@mikro-orm/sqlite';
+
+// eslint-disable-next-line import/no-cycle
+import { Tab } from './tab.entity';
+
+export class TabsRepository extends EntityRepository {}
diff --git a/src/main/database/entities/tls-presets/index.ts b/src/main/database/entities/tls-presets/index.ts
new file mode 100644
index 00000000..408996e7
--- /dev/null
+++ b/src/main/database/entities/tls-presets/index.ts
@@ -0,0 +1,3 @@
+export * from './tls-preset.entity';
+export * from './tls-presets.repository';
+export * from './tls-preset.mapper';
diff --git a/src/main/database/entities/tls-presets/tls-preset.entity.ts b/src/main/database/entities/tls-presets/tls-preset.entity.ts
new file mode 100644
index 00000000..1594019f
--- /dev/null
+++ b/src/main/database/entities/tls-presets/tls-preset.entity.ts
@@ -0,0 +1,85 @@
+/* eslint-disable max-classes-per-file */
+
+import { AutoMap } from '@automapper/classes';
+import {
+ GrpcChannelOptions as IGrpcChannelOptions,
+ GrpcInsecureTlsConfig,
+ GrpcMutualTlsConfig,
+ GrpcServerSideTlsConfig,
+ GrpcTlsType,
+} from '@getezy/grpc-client';
+import {
+ Embeddable,
+ Embedded,
+ Entity,
+ EntityRepositoryType,
+ Enum,
+ PrimaryKey,
+ Property,
+} from '@mikro-orm/core';
+
+// eslint-disable-next-line import/no-cycle
+import { TlsPresetsRepository } from './tls-presets.repository';
+
+@Embeddable()
+export class GrpcChannelOptions implements IGrpcChannelOptions {
+ @Property({ nullable: true })
+ sslTargetNameOverride?: string;
+}
+
+@Embeddable({ abstract: true, discriminatorColumn: 'type' })
+export abstract class TLS {
+ @Enum({ type: 'string', items: () => GrpcTlsType })
+ type!: GrpcTlsType;
+}
+
+@Embeddable({ discriminatorValue: GrpcTlsType.INSECURE })
+export class InsecureTls extends TLS implements GrpcInsecureTlsConfig {
+ declare type: GrpcTlsType.INSECURE;
+}
+
+@Embeddable({ discriminatorValue: GrpcTlsType.SERVER_SIDE })
+export class ServerSideTls extends TLS implements GrpcServerSideTlsConfig {
+ declare type: GrpcTlsType.SERVER_SIDE;
+
+ @Property({ nullable: true })
+ rootCertificatePath?: string;
+}
+
+@Embeddable({ discriminatorValue: GrpcTlsType.MUTUAL })
+export class MutualTls extends TLS implements GrpcMutualTlsConfig {
+ declare type: GrpcTlsType.MUTUAL;
+
+ @Property()
+ clientCertificatePath!: string;
+
+ @Property()
+ clientKeyPath!: string;
+
+ @Property({ nullable: true })
+ rootCertificatePath?: string;
+}
+
+@Entity({ tableName: 'tls_presets', customRepository: () => TlsPresetsRepository })
+export class TlsPreset {
+ [EntityRepositoryType]?: TlsPresetsRepository;
+
+ @PrimaryKey()
+ @AutoMap()
+ id!: string;
+
+ @Property()
+ @AutoMap()
+ name!: string;
+
+ @Property({ default: false })
+ @AutoMap()
+ system: boolean = false;
+
+ @Embedded(() => [InsecureTls, ServerSideTls, MutualTls], { object: true })
+ tls!: InsecureTls | ServerSideTls | MutualTls;
+
+ @Embedded(() => GrpcChannelOptions, { nullable: true, object: true })
+ @AutoMap()
+ channelOptions?: GrpcChannelOptions;
+}
diff --git a/src/main/database/entities/tls-presets/tls-preset.mapper.ts b/src/main/database/entities/tls-presets/tls-preset.mapper.ts
new file mode 100644
index 00000000..166c0f54
--- /dev/null
+++ b/src/main/database/entities/tls-presets/tls-preset.mapper.ts
@@ -0,0 +1,19 @@
+import { constructUsing, createMap, forMember, mapFrom } from '@automapper/core';
+
+import { TlsPreset as TlsPresetView } from '@core';
+
+import { mapper } from '../../common';
+import { TlsPreset } from './tls-preset.entity';
+
+export function createTlsPresetMappings() {
+ createMap(
+ mapper,
+ TlsPreset,
+ TlsPresetView,
+ forMember(
+ (destination) => destination.tls,
+ mapFrom((source) => source.tls)
+ ),
+ constructUsing((sourceObject) => new TlsPresetView(sourceObject))
+ );
+}
diff --git a/src/main/database/entities/tls-presets/tls-presets.repository.ts b/src/main/database/entities/tls-presets/tls-presets.repository.ts
new file mode 100644
index 00000000..c37d5c3b
--- /dev/null
+++ b/src/main/database/entities/tls-presets/tls-presets.repository.ts
@@ -0,0 +1,6 @@
+import { EntityRepository } from '@mikro-orm/sqlite';
+
+// eslint-disable-next-line import/no-cycle
+import { TlsPreset } from './tls-preset.entity';
+
+export class TlsPresetsRepository extends EntityRepository {}
diff --git a/src/main/database/index.ts b/src/main/database/index.ts
new file mode 100644
index 00000000..94419105
--- /dev/null
+++ b/src/main/database/index.ts
@@ -0,0 +1,3 @@
+export * from './init-database';
+export * from './subscribers';
+export * from './preload';
diff --git a/src/main/database/init-database.ts b/src/main/database/init-database.ts
new file mode 100644
index 00000000..82ed21dd
--- /dev/null
+++ b/src/main/database/init-database.ts
@@ -0,0 +1,18 @@
+import { MikroORM } from '@mikro-orm/core';
+import { SqliteDriver } from '@mikro-orm/sqlite';
+
+import { DATABSE_PATH } from './common/database-path';
+import { knex } from './common/knex';
+import { Environment, Setting, Tab, TlsPreset } from './entities';
+
+export async function initDatabase() {
+ await knex.migrate.up();
+
+ const orm = await MikroORM.init({
+ entities: [Setting, Environment, TlsPreset, Tab],
+ dbName: DATABSE_PATH,
+ type: 'sqlite',
+ });
+
+ return orm;
+}
diff --git a/src/main/database/preload.ts b/src/main/database/preload.ts
new file mode 100644
index 00000000..cd27e5f0
--- /dev/null
+++ b/src/main/database/preload.ts
@@ -0,0 +1,16 @@
+import {
+ IAbstractTab as TabView,
+ IEnvironment as EnvironmentView,
+ ISetting as SettingView,
+ ITlsPreset as TlsPresetView,
+} from '@core';
+
+import { PreloadFactory } from './common';
+import { Environment, Setting, Tab, TlsPreset } from './entities';
+
+export const Database = {
+ settings: PreloadFactory.create(Setting),
+ environment: PreloadFactory.create(Environment),
+ tlsPresets: PreloadFactory.create(TlsPreset),
+ tabs: PreloadFactory.create(Tab),
+};
diff --git a/src/main/database/subscribers.ts b/src/main/database/subscribers.ts
new file mode 100644
index 00000000..1ff26b40
--- /dev/null
+++ b/src/main/database/subscribers.ts
@@ -0,0 +1,40 @@
+import { MikroORM } from '@mikro-orm/core';
+import { SqliteDriver } from '@mikro-orm/sqlite';
+
+import {
+ AbstractTab as TabView,
+ Environment as EnvironmentView,
+ Setting as SettingView,
+ TlsPreset as TlsPresetView,
+} from '@core';
+
+import { SubscriberFactory } from './common';
+import {
+ createEnvironmentMappings,
+ createSettingMappings,
+ createTabMappings,
+ createTlsPresetMappings,
+ Environment,
+ Setting,
+ Tab,
+ TlsPreset,
+} from './entities';
+
+function createMappings() {
+ createSettingMappings();
+ createEnvironmentMappings();
+ createTlsPresetMappings();
+ createTabMappings();
+}
+
+function createSubscribers(orm: MikroORM) {
+ SubscriberFactory.create(orm, Setting, SettingView);
+ SubscriberFactory.create(orm, Environment, EnvironmentView);
+ SubscriberFactory.create(orm, TlsPreset, TlsPresetView);
+ SubscriberFactory.create(orm, Tab, TabView);
+}
+
+export function registerDatabaseSubscribers(orm: MikroORM) {
+ createMappings();
+ createSubscribers(orm);
+}
diff --git a/src/main/index.ts b/src/main/index.ts
index dcf35345..7d0ee7d8 100644
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -1,4 +1,6 @@
-import { app, BrowserWindow } from 'electron';
+import { electronApp, is, optimizer } from '@electron-toolkit/utils';
+import { app, BrowserWindow, shell } from 'electron';
+import * as path from 'path';
import {
registerGrpcClientSubscribers,
@@ -6,47 +8,94 @@ import {
unregisterGrpcClientSubscribers,
unregisterGrpcWebClientSubscribers,
} from './clients';
+import { initDatabase, registerDatabaseSubscribers } from './database';
import { registerDialogSubscribers, unregisterDialogSubscribers } from './dialog';
import { registerElectronStoreSubscribers } from './electron-store';
import { registerOSSubscribers } from './os';
import { registerProtobufSubscribers } from './protobuf';
+import { registerSplashScreenSubscribers, SplashScreenEmitter } from './splash-screen';
-// This allows TypeScript to pick up the magic constant that's auto-generated by Forge's Webpack
-// plugin that tells the Electron app where to look for the Webpack-bundled app code (depending on
-// whether you're running in development or production).
-declare const MAIN_WINDOW_WEBPACK_ENTRY: string;
-declare const MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY: string;
-
-// Handle creating/removing shortcuts on Windows when installing/uninstalling.
-// eslint-disable-next-line global-require
-if (require('electron-squirrel-startup')) {
- // eslint-disable-line global-require
- app.quit();
-}
-
+registerSplashScreenSubscribers();
registerOSSubscribers();
registerElectronStoreSubscribers();
registerProtobufSubscribers();
-const createWindow = (): void => {
- // Create the browser window.
+const createSplashScreen = (): BrowserWindow => {
+ const splashScreen = new BrowserWindow({
+ show: false,
+ height: 280,
+ width: 520,
+ resizable: false,
+ frame: false,
+ center: true,
+ ...(process.platform === 'linux'
+ ? {
+ icon: path.join(__dirname, '../../resources/icons/icon.png'),
+ }
+ : {}),
+ webPreferences: {
+ preload: path.join(__dirname, '../preload/splash.js'),
+ sandbox: false,
+ },
+ });
+
+ splashScreen.webContents.setWindowOpenHandler((details) => {
+ if (details.url === 'about:blank') {
+ return {
+ action: 'allow',
+ };
+ }
+
+ shell.openExternal(details.url);
+ return { action: 'deny' };
+ });
+
+ // HMR for renderer base on electron-vite cli.
+ // Load the remote URL for development or the local html file for production.
+ if (is.dev && process.env.ELECTRON_RENDERER_URL) {
+ splashScreen.loadURL(`${process.env.ELECTRON_RENDERER_URL}/splash-screen/index.html`);
+ } else {
+ splashScreen.loadFile(path.join(__dirname, '../ui/splash-screen/index.html'));
+ }
+
+ return splashScreen;
+};
+
+function createWindow(): void {
const mainWindow = new BrowserWindow({
+ show: false,
height: 600,
width: 1000,
minHeight: 600,
minWidth: 800,
center: true,
+ ...(process.platform === 'linux'
+ ? {
+ icon: path.join(__dirname, '../../resources/icons/icon.png'),
+ }
+ : {}),
webPreferences: {
- preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,
+ preload: path.join(__dirname, '../preload/app.js'),
sandbox: false,
},
});
- // and load the index.html of the app.
- mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);
+ mainWindow.on('ready-to-show', () => {
+ mainWindow.show();
+ });
+
+ mainWindow.webContents.setWindowOpenHandler((details) => {
+ shell.openExternal(details.url);
+ return { action: 'deny' };
+ });
- // Open the DevTools.
- // mainWindow.webContents.openDevTools();
+ // HMR for renderer base on electron-vite cli.
+ // Load the remote URL for development or the local html file for production.
+ if (is.dev && process.env.ELECTRON_RENDERER_URL) {
+ mainWindow.loadURL(`${process.env.ELECTRON_RENDERER_URL}/app/index.html`);
+ } else {
+ mainWindow.loadFile(path.join(__dirname, '../ui/app/index.html'));
+ }
registerDialogSubscribers(mainWindow);
registerGrpcClientSubscribers(mainWindow);
@@ -57,12 +106,55 @@ const createWindow = (): void => {
unregisterGrpcClientSubscribers();
unregisterGrpcWebClientSubscribers();
});
-};
+}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
-app.on('ready', createWindow);
+app.whenReady().then(async () => {
+ // Set app user model id for windows
+ electronApp.setAppUserModelId('com.getezy.ezy');
+
+ const splashScreen = createSplashScreen();
+
+ splashScreen.on('ready-to-show', async () => {
+ splashScreen.show();
+
+ SplashScreenEmitter.sendLoaderText(splashScreen, 'Initialazing');
+
+ let orm;
+
+ try {
+ orm = await initDatabase();
+ } catch (error) {
+ SplashScreenEmitter.sendError(splashScreen, error);
+ return;
+ }
+
+ registerDatabaseSubscribers(orm);
+
+ setTimeout(() => {
+ splashScreen.close();
+
+ createWindow();
+ }, 2000);
+ });
+
+ // Default open or close DevTools by F12 in development
+ // and ignore CommandOrControl + R in production.
+ // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
+ app.on('browser-window-created', (_, window) => {
+ optimizer.watchWindowShortcuts(window);
+ });
+
+ app.on('activate', () => {
+ // On macOS it's common to re-create a window in the app when the
+ // dock icon is clicked and there are no other windows open.
+ if (BrowserWindow.getAllWindows().length === 0) {
+ createWindow();
+ }
+ });
+});
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
@@ -73,13 +165,5 @@ app.on('window-all-closed', () => {
}
});
-app.on('activate', () => {
- // On OS X it's common to re-create a window in the app when the
- // dock icon is clicked and there are no other windows open.
- if (BrowserWindow.getAllWindows().length === 0) {
- createWindow();
- }
-});
-
-// In this file you can include the rest of your app's specific main process
-// code. You can also put them in separate files and import them here.
+// In this file you can include the rest of your app"s specific main process
+// code. You can also put them in separate files and require them here.
diff --git a/src/main/new-clients/grpc/constants.ts b/src/main/new-clients/grpc/constants.ts
new file mode 100644
index 00000000..92a8e9c7
--- /dev/null
+++ b/src/main/new-clients/grpc/constants.ts
@@ -0,0 +1,49 @@
+/**
+ * Represents channel from renderer process
+ */
+export enum GrpcClientChannel {
+ INVOKE_UNARY_REQUEST = 'grpc-client-channel:unary-request:invoke',
+
+ INVOKE_SERVER_STREAMING_REQUEST = 'grpc-client-channel:server-streaming-request:invoke',
+ CANCEL_SERVER_STREAMING_REQUEST = 'grpc-client-channel:server-streaming-request:cancel',
+
+ INVOKE_CLIENT_STREAMING_REQUEST = 'grpc-client-channel:client-streaming-request:invoke',
+ SEND_CLIENT_STREAMING_REQUEST = 'grpc-client-channel:client-streaming-request:send',
+ END_CLIENT_STREAMING_REQUEST = 'grpc-client-channel:client-streaming-request:end',
+ CANCEL_CLIENT_STREAMING_REQUEST = 'grpc-client-channel:client-streaming-request:cancel',
+
+ INVOKE_BIDIRECTIONAL_STREAMING_REQUEST = 'grpc-client-channel:bidirectional-streaming-request:invoke',
+ SEND_BIDIRECTIONAL_STREAMING_REQUEST = 'grpc-client-channel:bidirectional-streaming-request:send',
+ END_BIDIRECTIONAL_STREAMING_REQUEST = 'grpc-client-channel:bidirectional-streaming-request:end',
+ CANCEL_BIDIRECTIONAL_STREAMING_REQUEST = 'grpc-client-channel:bidirectional-streaming-request:cancel',
+}
+
+/**
+ * Represents channel from main process
+ */
+export enum GrpcServerStreamingChannel {
+ RESPONSE = 'grpc-client:server-streaming-request:response',
+ ERROR = 'grpc-client:server-streaming-request:error',
+ END = 'grpc-client:server-streaming-request:end',
+ CANCEL = 'grpc-client:server-streaming-request:cancel',
+}
+
+/**
+ * Represents channel from main process
+ */
+export enum GrpcClientStreamingChannel {
+ RESPONSE = 'grpc-client:client-streaming-request:RESPONSE',
+ ERROR = 'grpc-client:client-streaming-request:error',
+ END = 'grpc-client:client-streaming-request:end',
+ CANCEL = 'grpc-client:client-streaming-request:cancel',
+}
+
+/**
+ * Represents channel from main process
+ */
+export enum GrpcBidirectionalStreamingChannel {
+ RESPONSE = 'grpc-client:bidirectional-streaming-request:RESPONSE',
+ ERROR = 'grpc-client:bidirectional-streaming-request:error',
+ END = 'grpc-client:bidirectional-streaming-request:end',
+ CANCEL = 'grpc-client:bidirectional-streaming-request:cancel',
+}
diff --git a/src/main/new-clients/grpc/index.ts b/src/main/new-clients/grpc/index.ts
new file mode 100644
index 00000000..2103a66e
--- /dev/null
+++ b/src/main/new-clients/grpc/index.ts
@@ -0,0 +1,2 @@
+export * from './preload';
+export * from './subscribers';
diff --git a/src/main/new-clients/grpc/interfaces/grpc-loader-options.interface.ts b/src/main/new-clients/grpc/interfaces/grpc-loader-options.interface.ts
new file mode 100644
index 00000000..8e5a908d
--- /dev/null
+++ b/src/main/new-clients/grpc/interfaces/grpc-loader-options.interface.ts
@@ -0,0 +1,7 @@
+import { ProtobufLoaderOptions } from '@getezy/grpc-client';
+
+import { GrpcLoaderType } from './grpc-loader-type.enum';
+
+export type LoaderOptions = T extends GrpcLoaderType.Protobuf
+ ? ProtobufLoaderOptions
+ : never;
diff --git a/src/main/new-clients/grpc/interfaces/grpc-loader-type.enum.ts b/src/main/new-clients/grpc/interfaces/grpc-loader-type.enum.ts
new file mode 100644
index 00000000..2408fe67
--- /dev/null
+++ b/src/main/new-clients/grpc/interfaces/grpc-loader-type.enum.ts
@@ -0,0 +1,3 @@
+export enum GrpcLoaderType {
+ Protobuf = 'protobuf',
+}
diff --git a/src/main/new-clients/grpc/interfaces/grpc-metadata.interface.ts b/src/main/new-clients/grpc/interfaces/grpc-metadata.interface.ts
new file mode 100644
index 00000000..5a462860
--- /dev/null
+++ b/src/main/new-clients/grpc/interfaces/grpc-metadata.interface.ts
@@ -0,0 +1,15 @@
+import { AbstractProtocol, GrpcMetadataValue, GrpcWebMetadataValue } from '@getezy/grpc-client';
+
+import { GrpcProtocolType } from '@core';
+
+export type AbstractProtocolMetadataValue = T extends AbstractProtocol
+ ? MetadataValue
+ : T;
+
+export type AbstractProtocolMetadata = T extends AbstractProtocol
+ ? Metadata
+ : T;
+
+export type GrpcMetadata = T extends GrpcProtocolType.Grpc
+ ? Record
+ : Record;
diff --git a/src/main/new-clients/grpc/interfaces/grpc-protocol-options.interface.ts b/src/main/new-clients/grpc/interfaces/grpc-protocol-options.interface.ts
new file mode 100644
index 00000000..fcf3d099
--- /dev/null
+++ b/src/main/new-clients/grpc/interfaces/grpc-protocol-options.interface.ts
@@ -0,0 +1,7 @@
+import { GrpcProtocolOptions, GrpcWebProtocolOptions } from '@getezy/grpc-client';
+
+import { GrpcProtocolType } from '@core';
+
+export type ProtocolOptions = T extends GrpcProtocolType.Grpc
+ ? GrpcProtocolOptions
+ : GrpcWebProtocolOptions;
diff --git a/src/main/new-clients/grpc/interfaces/index.ts b/src/main/new-clients/grpc/interfaces/index.ts
new file mode 100644
index 00000000..6517b077
--- /dev/null
+++ b/src/main/new-clients/grpc/interfaces/index.ts
@@ -0,0 +1,4 @@
+export * from './grpc-loader-options.interface';
+export * from './grpc-loader-type.enum';
+export * from './grpc-metadata.interface';
+export * from './grpc-protocol-options.interface';
diff --git a/src/main/new-clients/grpc/preload/bidirectional-streaming.preload.ts b/src/main/new-clients/grpc/preload/bidirectional-streaming.preload.ts
new file mode 100644
index 00000000..13349d7e
--- /dev/null
+++ b/src/main/new-clients/grpc/preload/bidirectional-streaming.preload.ts
@@ -0,0 +1,98 @@
+/* eslint-disable @typescript-eslint/no-use-before-define */
+
+import { GrpcRequestOptions, GrpcResponse } from '@getezy/grpc-client';
+import { ipcRenderer } from 'electron';
+
+import { GrpcProtocolType } from '@core';
+
+import { parseErrorFromIPCMain } from '../../../common';
+import { GrpcBidirectionalStreamingChannel, GrpcClientChannel } from '../constants';
+import { GrpcLoaderType, GrpcMetadata, LoaderOptions, ProtocolOptions } from '../interfaces';
+import { OnEndCallback, OnErrorCallback, OnResponseCallback, wrapHandler } from './handlers';
+
+export default {
+ async invoke(
+ source: string,
+ loaderType: Loader,
+ loaderOptions: LoaderOptions,
+ protocolType: Protocol,
+ protocolOptions: ProtocolOptions,
+ requestOptions: GrpcRequestOptions,
+ metadata: GrpcMetadata,
+ onResponse: OnResponseCallback,
+ onError: OnErrorCallback,
+ onEnd: OnEndCallback
+ ): Promise {
+ try {
+ const streamId = await ipcRenderer.invoke(
+ GrpcClientChannel.INVOKE_BIDIRECTIONAL_STREAMING_REQUEST,
+ source,
+ loaderType,
+ loaderOptions,
+ protocolType,
+ protocolOptions,
+ requestOptions,
+ metadata
+ );
+
+ const onResponseCallback = wrapHandler(streamId, (response: GrpcResponse) => {
+ onResponse(response);
+ });
+
+ const onErrorCallback = wrapHandler(streamId, (error: GrpcResponse) => {
+ onError(error);
+ removeListeners();
+ });
+
+ const onEndServerCallback = wrapHandler(streamId, () => {
+ onEnd();
+ removeListeners();
+ });
+
+ const onCancelCallback = wrapHandler(streamId, () => {
+ removeListeners();
+ });
+
+ const removeListeners = () => {
+ ipcRenderer.removeListener(GrpcBidirectionalStreamingChannel.RESPONSE, onResponseCallback);
+ ipcRenderer.removeListener(GrpcBidirectionalStreamingChannel.ERROR, onErrorCallback);
+ ipcRenderer.removeListener(GrpcBidirectionalStreamingChannel.END, onEndServerCallback);
+ ipcRenderer.removeListener(GrpcBidirectionalStreamingChannel.CANCEL, onCancelCallback);
+ };
+
+ ipcRenderer.on(GrpcBidirectionalStreamingChannel.RESPONSE, onResponseCallback);
+ ipcRenderer.on(GrpcBidirectionalStreamingChannel.ERROR, onErrorCallback);
+ ipcRenderer.on(GrpcBidirectionalStreamingChannel.END, onEndServerCallback);
+ ipcRenderer.on(GrpcBidirectionalStreamingChannel.CANCEL, onCancelCallback);
+
+ return streamId;
+ } catch (error) {
+ throw new Error(parseErrorFromIPCMain(error));
+ }
+ },
+
+ async send(id: string, payload: Record): Promise {
+ try {
+ await ipcRenderer.invoke(GrpcClientChannel.SEND_BIDIRECTIONAL_STREAMING_REQUEST, id, payload);
+ } catch (error) {
+ throw new Error(parseErrorFromIPCMain(error));
+ }
+ },
+
+ async end(id: string): Promise {
+ try {
+ await ipcRenderer.invoke(GrpcClientChannel.END_BIDIRECTIONAL_STREAMING_REQUEST, id);
+ } catch (error) {
+ throw new Error(parseErrorFromIPCMain(error));
+ }
+ },
+
+ async cancel(id: string): Promise {
+ try {
+ ipcRenderer.emit(GrpcBidirectionalStreamingChannel.CANCEL, id);
+ await ipcRenderer.invoke(GrpcClientChannel.CANCEL_BIDIRECTIONAL_STREAMING_REQUEST, id);
+ } catch (error) {
+ throw new Error(parseErrorFromIPCMain(error));
+ }
+ },
+};
diff --git a/src/main/new-clients/grpc/preload/client-streaming.preload.ts b/src/main/new-clients/grpc/preload/client-streaming.preload.ts
new file mode 100644
index 00000000..444a587c
--- /dev/null
+++ b/src/main/new-clients/grpc/preload/client-streaming.preload.ts
@@ -0,0 +1,97 @@
+/* eslint-disable @typescript-eslint/no-use-before-define */
+
+import { GrpcRequestOptions, GrpcRequestValue, GrpcResponse } from '@getezy/grpc-client';
+import { ipcRenderer } from 'electron';
+
+import { GrpcProtocolType } from '@core';
+
+import { parseErrorFromIPCMain } from '../../../common';
+import { GrpcClientChannel, GrpcClientStreamingChannel } from '../constants';
+import { GrpcLoaderType, GrpcMetadata, LoaderOptions, ProtocolOptions } from '../interfaces';
+import { OnErrorCallback, OnResponseCallback, wrapHandler } from './handlers';
+
+export default {
+ async invoke(
+ source: string,
+ loaderType: Loader,
+ loaderOptions: LoaderOptions,
+ protocolType: Protocol,
+ protocolOptions: ProtocolOptions,
+ requestOptions: GrpcRequestOptions,
+ metadata: GrpcMetadata,
+ onResponse: OnResponseCallback,
+ onError: OnErrorCallback
+ ): Promise {
+ try {
+ const streamId = await ipcRenderer.invoke(
+ GrpcClientChannel.INVOKE_CLIENT_STREAMING_REQUEST,
+ source,
+ loaderType,
+ loaderOptions,
+ protocolType,
+ protocolOptions,
+ requestOptions,
+ metadata
+ );
+
+ const onResponseCallback = wrapHandler(streamId, (response: GrpcResponse) => {
+ onResponse(response);
+ });
+
+ const onErrorCallback = wrapHandler(streamId, (error: GrpcResponse) => {
+ onError(error);
+ removeListeners();
+ });
+
+ const onEndCallback = wrapHandler(streamId, () => {
+ removeListeners();
+ });
+
+ const onCancelCallback = wrapHandler(streamId, () => {
+ removeListeners();
+ });
+
+ const removeListeners = () => {
+ ipcRenderer.removeListener(GrpcClientStreamingChannel.RESPONSE, onResponseCallback);
+ ipcRenderer.removeListener(GrpcClientStreamingChannel.ERROR, onErrorCallback);
+ ipcRenderer.removeListener(GrpcClientStreamingChannel.END, onEndCallback);
+ ipcRenderer.removeListener(GrpcClientStreamingChannel.CANCEL, onCancelCallback);
+ };
+
+ ipcRenderer.on(GrpcClientStreamingChannel.RESPONSE, onResponseCallback);
+ ipcRenderer.on(GrpcClientStreamingChannel.ERROR, onErrorCallback);
+ ipcRenderer.on(GrpcClientStreamingChannel.END, onEndCallback);
+ ipcRenderer.on(GrpcClientStreamingChannel.CANCEL, onCancelCallback);
+
+ return streamId;
+ } catch (error) {
+ throw new Error(parseErrorFromIPCMain(error));
+ }
+ },
+
+ async send(id: string, payload: GrpcRequestValue): Promise {
+ try {
+ await ipcRenderer.invoke(GrpcClientChannel.SEND_CLIENT_STREAMING_REQUEST, id, payload);
+ } catch (error) {
+ throw new Error(parseErrorFromIPCMain(error));
+ }
+ },
+
+ async end(id: string): Promise {
+ try {
+ ipcRenderer.emit(GrpcClientStreamingChannel.END, id);
+ await ipcRenderer.invoke(GrpcClientChannel.END_CLIENT_STREAMING_REQUEST, id);
+ } catch (error) {
+ throw new Error(parseErrorFromIPCMain(error));
+ }
+ },
+
+ async cancel(id: string): Promise {
+ try {
+ ipcRenderer.emit(GrpcClientStreamingChannel.CANCEL, id);
+ await ipcRenderer.invoke(GrpcClientChannel.CANCEL_CLIENT_STREAMING_REQUEST, id);
+ } catch (error) {
+ throw new Error(parseErrorFromIPCMain(error));
+ }
+ },
+};
diff --git a/src/main/new-clients/grpc/preload/handlers.ts b/src/main/new-clients/grpc/preload/handlers.ts
new file mode 100644
index 00000000..bde6ef36
--- /dev/null
+++ b/src/main/new-clients/grpc/preload/handlers.ts
@@ -0,0 +1,16 @@
+import { GrpcResponse } from '@getezy/grpc-client';
+import { IpcRendererEvent } from 'electron';
+
+export type OnResponseCallback = (response: GrpcResponse) => void;
+
+export type OnErrorCallback = (error: GrpcResponse) => void;
+
+export type OnEndCallback = () => void;
+
+export function wrapHandler(streamId: string, callback: (...callbackArgs: any[]) => void) {
+ return function wrappedHandler(_event: IpcRendererEvent, id: string, ...args: any[]) {
+ if (streamId === id) {
+ callback(...args);
+ }
+ };
+}
diff --git a/src/main/new-clients/grpc/preload/index.ts b/src/main/new-clients/grpc/preload/index.ts
new file mode 100644
index 00000000..be8e47f3
--- /dev/null
+++ b/src/main/new-clients/grpc/preload/index.ts
@@ -0,0 +1,11 @@
+import bidirectionalStreaming from './bidirectional-streaming.preload';
+import clientStreaming from './client-streaming.preload';
+import serverStreaming from './server-streaming.preload';
+import unary from './unary.preload';
+
+export const GrpcClient = {
+ unary,
+ clientStreaming,
+ serverStreaming,
+ bidirectionalStreaming,
+};
diff --git a/src/main/new-clients/grpc/preload/server-streaming.preload.ts b/src/main/new-clients/grpc/preload/server-streaming.preload.ts
new file mode 100644
index 00000000..5655c522
--- /dev/null
+++ b/src/main/new-clients/grpc/preload/server-streaming.preload.ts
@@ -0,0 +1,84 @@
+/* eslint-disable @typescript-eslint/no-use-before-define */
+
+import { GrpcRequestOptions, GrpcRequestValue, GrpcResponse } from '@getezy/grpc-client';
+import { ipcRenderer } from 'electron';
+
+import { GrpcProtocolType } from '@core';
+
+import { parseErrorFromIPCMain } from '../../../common';
+import { GrpcClientChannel, GrpcServerStreamingChannel } from '../constants';
+import { GrpcLoaderType, GrpcMetadata, LoaderOptions, ProtocolOptions } from '../interfaces';
+import { OnEndCallback, OnErrorCallback, OnResponseCallback, wrapHandler } from './handlers';
+
+export default {
+ async invoke(
+ source: string,
+ loaderType: Loader,
+ loaderOptions: LoaderOptions,
+ protocolType: Protocol,
+ protocolOptions: ProtocolOptions,
+ requestOptions: GrpcRequestOptions,
+ payload: GrpcRequestValue,
+ metadata: GrpcMetadata,
+ onResponse: OnResponseCallback,
+ onError: OnErrorCallback,
+ onEnd: OnEndCallback
+ ): Promise {
+ try {
+ const streamId = await ipcRenderer.invoke(
+ GrpcClientChannel.INVOKE_SERVER_STREAMING_REQUEST,
+ source,
+ loaderType,
+ loaderOptions,
+ protocolType,
+ protocolOptions,
+ requestOptions,
+ payload,
+ metadata
+ );
+
+ const onResponseCallback = wrapHandler(streamId, (response: GrpcResponse) => {
+ onResponse(response);
+ });
+
+ const onErrorCallback = wrapHandler(streamId, (error: GrpcResponse) => {
+ onError(error);
+ removeListeners();
+ });
+
+ const onEndCallback = wrapHandler(streamId, () => {
+ onEnd();
+ removeListeners();
+ });
+
+ const onCancelCallback = wrapHandler(streamId, () => {
+ removeListeners();
+ });
+
+ const removeListeners = () => {
+ ipcRenderer.removeListener(GrpcServerStreamingChannel.RESPONSE, onResponseCallback);
+ ipcRenderer.removeListener(GrpcServerStreamingChannel.ERROR, onErrorCallback);
+ ipcRenderer.removeListener(GrpcServerStreamingChannel.END, onEndCallback);
+ ipcRenderer.removeListener(GrpcServerStreamingChannel.CANCEL, onCancelCallback);
+ };
+
+ ipcRenderer.on(GrpcServerStreamingChannel.RESPONSE, onResponseCallback);
+ ipcRenderer.on(GrpcServerStreamingChannel.ERROR, onErrorCallback);
+ ipcRenderer.on(GrpcServerStreamingChannel.END, onEndCallback);
+ ipcRenderer.on(GrpcServerStreamingChannel.CANCEL, onCancelCallback);
+
+ return streamId;
+ } catch (error) {
+ throw new Error(parseErrorFromIPCMain(error));
+ }
+ },
+
+ async cancel(id: string): Promise {
+ try {
+ ipcRenderer.emit(GrpcServerStreamingChannel.CANCEL, id);
+ await ipcRenderer.invoke(GrpcClientChannel.CANCEL_SERVER_STREAMING_REQUEST, id);
+ } catch (error) {
+ throw new Error(parseErrorFromIPCMain(error));
+ }
+ },
+};
diff --git a/src/main/new-clients/grpc/preload/unary.preload.ts b/src/main/new-clients/grpc/preload/unary.preload.ts
new file mode 100644
index 00000000..87f546c5
--- /dev/null
+++ b/src/main/new-clients/grpc/preload/unary.preload.ts
@@ -0,0 +1,48 @@
+import {
+ GrpcRequestOptions,
+ GrpcRequestValue,
+ GrpcResponse,
+ GrpcResponseValue,
+} from '@getezy/grpc-client';
+import { ipcRenderer } from 'electron';
+
+import { GrpcProtocolType } from '@core';
+
+import { parseErrorFromIPCMain } from '../../../common';
+import { GrpcClientChannel } from '../constants';
+import { GrpcLoaderType, GrpcMetadata, LoaderOptions, ProtocolOptions } from '../interfaces';
+
+export default {
+ async invoke<
+ Protocol extends GrpcProtocolType,
+ Loader extends GrpcLoaderType,
+ Response extends GrpcResponseValue = GrpcResponseValue
+ >(
+ source: string,
+ loaderType: Loader,
+ loaderOptions: LoaderOptions,
+ protocolType: Protocol,
+ protocolOptions: ProtocolOptions,
+ requestOptions: GrpcRequestOptions,
+ payload: GrpcRequestValue,
+ metadata?: GrpcMetadata
+ ): Promise> {
+ try {
+ const response = await ipcRenderer.invoke(
+ GrpcClientChannel.INVOKE_UNARY_REQUEST,
+ source,
+ loaderType,
+ loaderOptions,
+ protocolType,
+ protocolOptions,
+ requestOptions,
+ payload,
+ metadata
+ );
+
+ return response;
+ } catch (error) {
+ throw new Error(parseErrorFromIPCMain(error));
+ }
+ },
+};
diff --git a/src/main/new-clients/grpc/subscribers/abstract.subscriber.ts b/src/main/new-clients/grpc/subscribers/abstract.subscriber.ts
new file mode 100644
index 00000000..cdf9bca0
--- /dev/null
+++ b/src/main/new-clients/grpc/subscribers/abstract.subscriber.ts
@@ -0,0 +1,37 @@
+/* eslint-disable class-methods-use-this */
+
+import { GrpcProtocol, GrpcWebProtocol, ProtobufLoader } from '@getezy/grpc-client';
+import { BrowserWindow, IpcMain } from 'electron';
+
+import { GrpcProtocolType } from '@core';
+
+import { GrpcLoaderType, LoaderOptions, ProtocolOptions } from '../interfaces';
+
+export abstract class AbstractSubscriber {
+ constructor(protected readonly mainWindow: BrowserWindow, protected readonly ipcMain: IpcMain) {}
+
+ protected createProtocol(
+ type: ProtocolType,
+ options: ProtocolOptions
+ ) {
+ if (type === GrpcProtocolType.GrpcWeb) {
+ return new GrpcWebProtocol(options);
+ }
+
+ return new GrpcProtocol(options);
+ }
+
+ protected createLoader(
+ type: GrpcLoaderType,
+ source: string,
+ options?: LoaderOptions
+ ) {
+ if (type === GrpcLoaderType.Protobuf) {
+ return new ProtobufLoader(source, options);
+ }
+
+ throw new Error('Unrecognized protobuf loader');
+ }
+
+ public abstract registerCallHandler(): void;
+}
diff --git a/src/main/new-clients/grpc/subscribers/bidirectional-streaming.subscriber.ts b/src/main/new-clients/grpc/subscribers/bidirectional-streaming.subscriber.ts
new file mode 100644
index 00000000..f652ebe5
--- /dev/null
+++ b/src/main/new-clients/grpc/subscribers/bidirectional-streaming.subscriber.ts
@@ -0,0 +1,112 @@
+import { BidirectionalStream, GrpcClientFactory, GrpcRequestOptions } from '@getezy/grpc-client';
+import { IpcMain, IpcMainInvokeEvent } from 'electron';
+import { v4 as uuid } from 'uuid';
+
+import { GrpcProtocolType } from '@core';
+
+import { GrpcBidirectionalStreamingChannel, GrpcClientChannel } from '../constants';
+import {
+ AbstractProtocolMetadata,
+ AbstractProtocolMetadataValue,
+ GrpcLoaderType,
+ GrpcMetadata,
+ LoaderOptions,
+ ProtocolOptions,
+} from '../interfaces';
+import { AbstractSubscriber } from './abstract.subscriber';
+
+export class GrpcBidirectionalStreamingSubscriber extends AbstractSubscriber {
+ private bidirectionalStreamingCalls = new Map();
+
+ public static unregisterCallHandlers(ipcMain: IpcMain) {
+ ipcMain.removeHandler(GrpcClientChannel.INVOKE_BIDIRECTIONAL_STREAMING_REQUEST);
+ ipcMain.removeHandler(GrpcClientChannel.SEND_BIDIRECTIONAL_STREAMING_REQUEST);
+ ipcMain.removeHandler(GrpcClientChannel.END_BIDIRECTIONAL_STREAMING_REQUEST);
+ ipcMain.removeHandler(GrpcClientChannel.CANCEL_BIDIRECTIONAL_STREAMING_REQUEST);
+ }
+
+ public registerCallHandler() {
+ this.ipcMain.handle(
+ GrpcClientChannel.INVOKE_BIDIRECTIONAL_STREAMING_REQUEST,
+ async (
+ _event: IpcMainInvokeEvent,
+ source: string,
+ loaderType: Loader,
+ loaderOptions: LoaderOptions,
+ protocolType: Protocol,
+ protocolOptions: ProtocolOptions,
+ requestOptions: GrpcRequestOptions,
+ metadata?: GrpcMetadata
+ ) => {
+ const protocol = this.createProtocol(protocolType, protocolOptions);
+ const loader = this.createLoader(loaderType, source, loaderOptions);
+
+ const client = await GrpcClientFactory.create<
+ AbstractProtocolMetadataValue,
+ AbstractProtocolMetadata
+ >(loader, protocol);
+
+ const stream = client.invokeBidirectionalStreamingRequest(requestOptions, metadata);
+
+ return this.subscribeOnStreamEvents(stream);
+ }
+ );
+
+ this.ipcMain.handle(
+ GrpcClientChannel.SEND_BIDIRECTIONAL_STREAMING_REQUEST,
+ (_event, id: string, payload: Record) => {
+ const call = this.bidirectionalStreamingCalls.get(id);
+
+ if (call) {
+ call.write(payload);
+ }
+ }
+ );
+
+ this.ipcMain.handle(
+ GrpcClientChannel.END_BIDIRECTIONAL_STREAMING_REQUEST,
+ (_event, id: string) => {
+ const call = this.bidirectionalStreamingCalls.get(id);
+
+ if (call) {
+ call.end();
+ }
+ }
+ );
+
+ this.ipcMain.handle(
+ GrpcClientChannel.CANCEL_BIDIRECTIONAL_STREAMING_REQUEST,
+ (_event, id: string) => {
+ const call = this.bidirectionalStreamingCalls.get(id);
+
+ if (call) {
+ call.cancel();
+ }
+
+ this.bidirectionalStreamingCalls.delete(id);
+ }
+ );
+ }
+
+ private subscribeOnStreamEvents(call: BidirectionalStream): string {
+ const id = uuid();
+
+ call.on('response', (response) => {
+ this.mainWindow.webContents.send(GrpcBidirectionalStreamingChannel.RESPONSE, id, response);
+ });
+
+ call.on('error', (error) => {
+ this.mainWindow.webContents.send(GrpcBidirectionalStreamingChannel.ERROR, id, error);
+ this.bidirectionalStreamingCalls.delete(id);
+ });
+
+ call.on('end-server-stream', () => {
+ this.mainWindow.webContents.send(GrpcBidirectionalStreamingChannel.END, id);
+ this.bidirectionalStreamingCalls.delete(id);
+ });
+
+ this.bidirectionalStreamingCalls.set(id, call);
+
+ return id;
+ }
+}
diff --git a/src/main/new-clients/grpc/subscribers/client-streaming.subscriber.ts b/src/main/new-clients/grpc/subscribers/client-streaming.subscriber.ts
new file mode 100644
index 00000000..f1da2b29
--- /dev/null
+++ b/src/main/new-clients/grpc/subscribers/client-streaming.subscriber.ts
@@ -0,0 +1,113 @@
+import {
+ ClientStream,
+ GrpcClientFactory,
+ GrpcRequestOptions,
+ GrpcRequestValue,
+} from '@getezy/grpc-client';
+import { IpcMain, IpcMainInvokeEvent } from 'electron';
+import { v4 as uuid } from 'uuid';
+
+import { GrpcProtocolType } from '@core';
+
+import { GrpcClientChannel, GrpcClientStreamingChannel } from '../constants';
+import {
+ AbstractProtocolMetadata,
+ AbstractProtocolMetadataValue,
+ GrpcLoaderType,
+ GrpcMetadata,
+ LoaderOptions,
+ ProtocolOptions,
+} from '../interfaces';
+import { AbstractSubscriber } from './abstract.subscriber';
+
+export class GrpcClientStreamingSubscriber extends AbstractSubscriber {
+ private clientStreamingCalls = new Map();
+
+ public static unregisterCallHandlers(ipcMain: IpcMain) {
+ ipcMain.removeHandler(GrpcClientChannel.INVOKE_CLIENT_STREAMING_REQUEST);
+ ipcMain.removeHandler(GrpcClientChannel.SEND_CLIENT_STREAMING_REQUEST);
+ ipcMain.removeHandler(GrpcClientChannel.END_CLIENT_STREAMING_REQUEST);
+ ipcMain.removeHandler(GrpcClientChannel.CANCEL_CLIENT_STREAMING_REQUEST);
+ }
+
+ public registerCallHandler() {
+ this.ipcMain.handle(
+ GrpcClientChannel.INVOKE_CLIENT_STREAMING_REQUEST,
+ async (
+ _event: IpcMainInvokeEvent,
+ source: string,
+ loaderType: Loader,
+ loaderOptions: LoaderOptions,
+ protocolType: Protocol,
+ protocolOptions: ProtocolOptions,
+ requestOptions: GrpcRequestOptions,
+ metadata?: GrpcMetadata
+ ) => {
+ const protocol = this.createProtocol(protocolType, protocolOptions);
+ const loader = this.createLoader(loaderType, source, loaderOptions);
+
+ const client = await GrpcClientFactory.create<
+ AbstractProtocolMetadataValue,
+ AbstractProtocolMetadata
+ >(loader, protocol);
+
+ const stream = client.invokeClientStreamingRequest(requestOptions, metadata);
+
+ return this.subscribeOnStreamEvents(stream);
+ }
+ );
+
+ this.ipcMain.handle(
+ GrpcClientChannel.SEND_CLIENT_STREAMING_REQUEST,
+ (_event, id: string, payload: GrpcRequestValue) => {
+ const call = this.clientStreamingCalls.get(id);
+
+ if (call) {
+ call.write(payload);
+ }
+ }
+ );
+
+ this.ipcMain.handle(GrpcClientChannel.END_CLIENT_STREAMING_REQUEST, (_event, id: string) => {
+ const call = this.clientStreamingCalls.get(id);
+
+ if (call) {
+ call.end();
+ }
+
+ this.clientStreamingCalls.delete(id);
+ });
+
+ this.ipcMain.handle(GrpcClientChannel.CANCEL_CLIENT_STREAMING_REQUEST, (_event, id: string) => {
+ const call = this.clientStreamingCalls.get(id);
+
+ if (call) {
+ call.cancel();
+ }
+
+ this.clientStreamingCalls.delete(id);
+ });
+ }
+
+ private subscribeOnStreamEvents(call: ClientStream): string {
+ const id = uuid();
+
+ call.on('response', (response) => {
+ this.mainWindow.webContents.send(GrpcClientStreamingChannel.RESPONSE, id, response);
+ });
+
+ call.on('error', (error) => {
+ this.mainWindow.webContents.send(GrpcClientStreamingChannel.ERROR, id, error);
+ this.clientStreamingCalls.delete(id);
+ });
+
+ call.on('end', () => {
+ this.mainWindow.webContents.send(GrpcClientStreamingChannel.END, id);
+ this.clientStreamingCalls.delete(id);
+ });
+
+ this.clientStreamingCalls.set(id, call);
+
+ return id;
+ }
+}
diff --git a/src/main/new-clients/grpc/subscribers/index.ts b/src/main/new-clients/grpc/subscribers/index.ts
new file mode 100644
index 00000000..fe97eb93
--- /dev/null
+++ b/src/main/new-clients/grpc/subscribers/index.ts
@@ -0,0 +1,25 @@
+import { BrowserWindow, ipcMain } from 'electron';
+
+import { GrpcBidirectionalStreamingSubscriber } from './bidirectional-streaming.subscriber';
+import { GrpcClientStreamingSubscriber } from './client-streaming.subscriber';
+import { GrpcServerStreamingSubscriber } from './server-streaming.subscriber';
+import { GrpcUnarySubscriber } from './unary.subscriber';
+
+export const registerGrpcClientSubscribers = (mainWindow: BrowserWindow) => {
+ const unary = new GrpcUnarySubscriber(mainWindow, ipcMain);
+ const client = new GrpcClientStreamingSubscriber(mainWindow, ipcMain);
+ const server = new GrpcServerStreamingSubscriber(mainWindow, ipcMain);
+ const bidirectional = new GrpcBidirectionalStreamingSubscriber(mainWindow, ipcMain);
+
+ unary.registerCallHandler();
+ client.registerCallHandler();
+ server.registerCallHandler();
+ bidirectional.registerCallHandler();
+};
+
+export const unregisterGrpcClientSubscribers = () => {
+ GrpcUnarySubscriber.unregisterCallHandlers(ipcMain);
+ GrpcClientStreamingSubscriber.unregisterCallHandlers(ipcMain);
+ GrpcServerStreamingSubscriber.unregisterCallHandlers(ipcMain);
+ GrpcBidirectionalStreamingSubscriber.unregisterCallHandlers(ipcMain);
+};
diff --git a/src/main/new-clients/grpc/subscribers/server-streaming.subscriber.ts b/src/main/new-clients/grpc/subscribers/server-streaming.subscriber.ts
new file mode 100644
index 00000000..152b557e
--- /dev/null
+++ b/src/main/new-clients/grpc/subscribers/server-streaming.subscriber.ts
@@ -0,0 +1,91 @@
+import {
+ GrpcClientFactory,
+ GrpcRequestOptions,
+ GrpcRequestValue,
+ ServerStream,
+} from '@getezy/grpc-client';
+import { IpcMain, IpcMainInvokeEvent } from 'electron';
+import { v4 as uuid } from 'uuid';
+
+import { GrpcProtocolType } from '@core';
+
+import { GrpcClientChannel, GrpcServerStreamingChannel } from '../constants';
+import {
+ AbstractProtocolMetadata,
+ AbstractProtocolMetadataValue,
+ GrpcLoaderType,
+ GrpcMetadata,
+ LoaderOptions,
+ ProtocolOptions,
+} from '../interfaces';
+import { AbstractSubscriber } from './abstract.subscriber';
+
+export class GrpcServerStreamingSubscriber extends AbstractSubscriber {
+ private serverStreamingCalls = new Map();
+
+ public static unregisterCallHandlers(ipcMain: IpcMain) {
+ ipcMain.removeHandler(GrpcClientChannel.INVOKE_SERVER_STREAMING_REQUEST);
+ ipcMain.removeHandler(GrpcClientChannel.CANCEL_SERVER_STREAMING_REQUEST);
+ }
+
+ public registerCallHandler() {
+ this.ipcMain.handle(
+ GrpcClientChannel.INVOKE_SERVER_STREAMING_REQUEST,
+ async (
+ _event: IpcMainInvokeEvent,
+ source: string,
+ loaderType: Loader,
+ loaderOptions: LoaderOptions,
+ protocolType: Protocol,
+ protocolOptions: ProtocolOptions,
+ requestOptions: GrpcRequestOptions,
+ payload: GrpcRequestValue,
+ metadata?: GrpcMetadata
+ ) => {
+ const protocol = this.createProtocol(protocolType, protocolOptions);
+ const loader = this.createLoader(loaderType, source, loaderOptions);
+
+ const client = await GrpcClientFactory.create<
+ AbstractProtocolMetadataValue,
+ AbstractProtocolMetadata
+ >(loader, protocol);
+
+ const stream = client.invokeServerStreamingRequest(requestOptions, payload, metadata);
+
+ return this.subscribeOnStreamEvents(stream);
+ }
+ );
+
+ this.ipcMain.handle(GrpcClientChannel.CANCEL_SERVER_STREAMING_REQUEST, (_event, id: string) => {
+ const call = this.serverStreamingCalls.get(id);
+
+ if (call) {
+ call.cancel();
+ }
+
+ this.serverStreamingCalls.delete(id);
+ });
+ }
+
+ private subscribeOnStreamEvents(call: ServerStream): string {
+ const id = uuid();
+
+ call.on('response', (response) => {
+ this.mainWindow.webContents.send(GrpcServerStreamingChannel.RESPONSE, id, response);
+ });
+
+ call.on('error', (error) => {
+ this.mainWindow.webContents.send(GrpcServerStreamingChannel.ERROR, id, error);
+ this.serverStreamingCalls.delete(id);
+ });
+
+ call.on('end', () => {
+ this.mainWindow.webContents.send(GrpcServerStreamingChannel.END, id);
+ this.serverStreamingCalls.delete(id);
+ });
+
+ this.serverStreamingCalls.set(id, call);
+
+ return id;
+ }
+}
diff --git a/src/main/new-clients/grpc/subscribers/unary.subscriber.ts b/src/main/new-clients/grpc/subscribers/unary.subscriber.ts
new file mode 100644
index 00000000..c6a561b7
--- /dev/null
+++ b/src/main/new-clients/grpc/subscribers/unary.subscriber.ts
@@ -0,0 +1,48 @@
+import { GrpcClientFactory, GrpcRequestOptions, GrpcRequestValue } from '@getezy/grpc-client';
+import { IpcMain, IpcMainInvokeEvent } from 'electron';
+
+import { GrpcProtocolType } from '@core';
+
+import { GrpcClientChannel } from '../constants';
+import {
+ AbstractProtocolMetadata,
+ AbstractProtocolMetadataValue,
+ GrpcLoaderType,
+ GrpcMetadata,
+ LoaderOptions,
+ ProtocolOptions,
+} from '../interfaces';
+import { AbstractSubscriber } from './abstract.subscriber';
+
+export class GrpcUnarySubscriber extends AbstractSubscriber {
+ public static unregisterCallHandlers(ipcMain: IpcMain) {
+ ipcMain.removeHandler(GrpcClientChannel.INVOKE_UNARY_REQUEST);
+ }
+
+ public registerCallHandler() {
+ this.ipcMain.handle(
+ GrpcClientChannel.INVOKE_UNARY_REQUEST,
+ async (
+ _event: IpcMainInvokeEvent,
+ source: string,
+ loaderType: Loader,
+ loaderOptions: LoaderOptions,
+ protocolType: Protocol,
+ protocolOptions: ProtocolOptions,
+ requestOptions: GrpcRequestOptions,
+ payload: GrpcRequestValue,
+ metadata?: GrpcMetadata
+ ) => {
+ const protocol = this.createProtocol(protocolType, protocolOptions);
+ const loader = this.createLoader(loaderType, source, loaderOptions);
+
+ const client = await GrpcClientFactory.create<
+ AbstractProtocolMetadataValue,
+ AbstractProtocolMetadata
+ >(loader, protocol);
+
+ return client.invokeUnaryRequest(requestOptions, payload, metadata);
+ }
+ );
+ }
+}
diff --git a/src/app/pages/collections/badge-types/index.ts b/src/main/new-clients/index.ts
similarity index 100%
rename from src/app/pages/collections/badge-types/index.ts
rename to src/main/new-clients/index.ts
diff --git a/src/main/preload.ts b/src/main/preload.ts
deleted file mode 100644
index d0f1475e..00000000
--- a/src/main/preload.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { contextBridge } from 'electron';
-
-import { GrpcClient, GrpcWebClient } from './clients';
-import { ElectronDialog } from './dialog';
-import { ElectronStore } from './electron-store';
-import { OS } from './os';
-import { Protobuf } from './protobuf';
-
-contextBridge.exposeInMainWorld('electronDialog', ElectronDialog);
-
-contextBridge.exposeInMainWorld('electronStore', ElectronStore);
-
-contextBridge.exposeInMainWorld('protobuf', Protobuf);
-
-contextBridge.exposeInMainWorld('clients', {
- grpc: GrpcClient,
- grpcWeb: GrpcWebClient,
-});
-
-contextBridge.exposeInMainWorld('os', OS);
diff --git a/src/main/protobuf/preload.ts b/src/main/protobuf/preload.ts
index 189dd3fe..18a657e5 100644
--- a/src/main/protobuf/preload.ts
+++ b/src/main/protobuf/preload.ts
@@ -1,12 +1,12 @@
import { ipcRenderer } from 'electron';
-import { GrpcOptions, GrpcServiceInfo } from '@core';
+import { GrpcOptions, GrpcServiceDefinition } from '@core';
import { parseErrorFromIPCMain } from '../common';
import { ProtobufChannel } from './constants';
export const Protobuf = {
- async loadFromFile(options: GrpcOptions): Promise {
+ async loadFromFile(options: GrpcOptions): Promise {
try {
const ast = await ipcRenderer.invoke(ProtobufChannel.LOAD_FROM_FILE, options);
diff --git a/src/main/splash-screen/constants.ts b/src/main/splash-screen/constants.ts
new file mode 100644
index 00000000..f3717fdc
--- /dev/null
+++ b/src/main/splash-screen/constants.ts
@@ -0,0 +1,5 @@
+export enum SplashScreenChannel {
+ LOADER_TEXT_CHANGE = 'splash-screen:loader-text-change',
+ ERROR = 'splash-screen:error',
+ QUIT = 'splash-screen:quit',
+}
diff --git a/src/main/splash-screen/emitter.ts b/src/main/splash-screen/emitter.ts
new file mode 100644
index 00000000..46e6f4a7
--- /dev/null
+++ b/src/main/splash-screen/emitter.ts
@@ -0,0 +1,25 @@
+import { BrowserWindow } from 'electron';
+
+import { SplashScreenChannel } from './constants';
+
+export class SplashScreenEmitter {
+ static sendLoaderText(window: BrowserWindow, text: string) {
+ window.webContents.send(SplashScreenChannel.LOADER_TEXT_CHANGE, text);
+ }
+
+ static sendError(window: BrowserWindow, error: any) {
+ let message: string;
+
+ if (error instanceof Error) {
+ message = JSON.stringify(
+ { name: error?.name, message: error?.message, stack: error?.stack, cause: error?.cause },
+ null,
+ 2
+ ).replace(/\\n/g, '\n');
+ } else {
+ message = error.toString();
+ }
+
+ window.webContents.send(SplashScreenChannel.ERROR, message);
+ }
+}
diff --git a/src/main/splash-screen/index.ts b/src/main/splash-screen/index.ts
new file mode 100644
index 00000000..dea63e12
--- /dev/null
+++ b/src/main/splash-screen/index.ts
@@ -0,0 +1,3 @@
+export * from './preload';
+export * from './emitter';
+export * from './subscribers';
diff --git a/src/main/splash-screen/preload.ts b/src/main/splash-screen/preload.ts
new file mode 100644
index 00000000..28d671df
--- /dev/null
+++ b/src/main/splash-screen/preload.ts
@@ -0,0 +1,19 @@
+import { ipcRenderer } from 'electron';
+
+import { SplashScreenChannel } from './constants';
+
+export const SplashScreen = {
+ handleLoaderTextChange: (callback: (text: string) => void) =>
+ ipcRenderer.on(SplashScreenChannel.LOADER_TEXT_CHANGE, (_event, text) => {
+ callback(text);
+ }),
+
+ handleError: (callback: (error: string) => void) =>
+ ipcRenderer.on(SplashScreenChannel.ERROR, (_event, error) => {
+ callback(error);
+ }),
+
+ quit(): Promise {
+ return ipcRenderer.invoke(SplashScreenChannel.QUIT);
+ },
+};
diff --git a/src/main/splash-screen/subscribers.ts b/src/main/splash-screen/subscribers.ts
new file mode 100644
index 00000000..e93f543f
--- /dev/null
+++ b/src/main/splash-screen/subscribers.ts
@@ -0,0 +1,7 @@
+import { app, ipcMain } from 'electron';
+
+import { SplashScreenChannel } from './constants';
+
+export const registerSplashScreenSubscribers = () => {
+ ipcMain.handle(SplashScreenChannel.QUIT, () => app.exit());
+};
diff --git a/src/preload/app/index.d.ts b/src/preload/app/index.d.ts
new file mode 100644
index 00000000..2b9a7f74
--- /dev/null
+++ b/src/preload/app/index.d.ts
@@ -0,0 +1,28 @@
+import { ElectronAPI } from '@electron-toolkit/preload';
+
+import { GrpcClient, GrpcWebClient } from '../../main/clients';
+import { Database } from '../../main/database/preload';
+import { ElectronDialog } from '../../main/dialog';
+import { ElectronStore } from '../../main/electron-store';
+import { GrpcClient as NewGrpcClient } from '../../main/new-clients';
+import { OS } from '../../main/os';
+import { Protobuf } from '../../main/protobuf';
+
+declare global {
+ interface Window {
+ electron: ElectronAPI;
+
+ database: typeof Database;
+ electronStore: typeof ElectronStore;
+ electronDialog: typeof ElectronDialog;
+ protobuf: typeof Protobuf;
+ newClients: {
+ grpc: typeof NewGrpcClient;
+ };
+ clients: {
+ grpc: typeof GrpcClient;
+ grpcWeb: typeof GrpcWebClient;
+ };
+ os: typeof OS;
+ }
+}
diff --git a/src/preload/app/index.ts b/src/preload/app/index.ts
new file mode 100644
index 00000000..057bdae1
--- /dev/null
+++ b/src/preload/app/index.ts
@@ -0,0 +1,63 @@
+import { electronAPI } from '@electron-toolkit/preload';
+import { contextBridge } from 'electron';
+
+import { GrpcClient, GrpcWebClient } from '../../main/clients';
+import { Database } from '../../main/database/preload';
+import { ElectronDialog } from '../../main/dialog';
+import { ElectronStore } from '../../main/electron-store';
+import { GrpcClient as NewGrpcClient } from '../../main/new-clients';
+import { OS } from '../../main/os';
+import { Protobuf } from '../../main/protobuf';
+
+// Use `contextBridge` APIs to expose Electron APIs to
+// renderer only if context isolation is enabled, otherwise
+// just add to the DOM global.
+if (process.contextIsolated) {
+ try {
+ contextBridge.exposeInMainWorld('electron', electronAPI);
+
+ contextBridge.exposeInMainWorld('electronDialog', ElectronDialog);
+
+ contextBridge.exposeInMainWorld('electronStore', ElectronStore);
+
+ contextBridge.exposeInMainWorld('protobuf', Protobuf);
+
+ contextBridge.exposeInMainWorld('clients', {
+ grpc: GrpcClient,
+ grpcWeb: GrpcWebClient,
+ });
+
+ contextBridge.exposeInMainWorld('newClients', {
+ grpc: NewGrpcClient,
+ });
+
+ contextBridge.exposeInMainWorld('os', OS);
+
+ contextBridge.exposeInMainWorld('database', Database);
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error(error);
+ }
+} else {
+ // @ts-ignore (define in dts)
+ window.electron = electronAPI;
+ // @ts-ignore (define in dts)
+ window.electronDialog = ElectronDialog;
+ // @ts-ignore (define in dts)
+ window.electronStore = ElectronStore;
+ // @ts-ignore (define in dts)
+ window.protobuf = Protobuf;
+ // @ts-ignore (define in dts)
+ window.clients = {
+ grpc: GrpcClient,
+ grpcWeb: GrpcWebClient,
+ };
+ // @ts-ignore (define in dts)
+ window.newClients = {
+ grpc: NewGrpcClient,
+ };
+ // @ts-ignore (define in dts)
+ window.os = OS;
+ // @ts-ignore (define in dts)
+ window.database = Database;
+}
diff --git a/src/preload/splash-screen/index.d.ts b/src/preload/splash-screen/index.d.ts
new file mode 100644
index 00000000..ace2d2b0
--- /dev/null
+++ b/src/preload/splash-screen/index.d.ts
@@ -0,0 +1,12 @@
+import { ElectronAPI } from '@electron-toolkit/preload';
+
+import { ElectronDialog } from '../../main/dialog';
+import { SplashScreen } from '../../main/splash-screen';
+
+declare global {
+ interface Window {
+ electron: ElectronAPI;
+ splashScreen: typeof SplashScreen;
+ electronDialog: typeof ElectronDialog;
+ }
+}
diff --git a/src/preload/splash-screen/index.ts b/src/preload/splash-screen/index.ts
new file mode 100644
index 00000000..58781247
--- /dev/null
+++ b/src/preload/splash-screen/index.ts
@@ -0,0 +1,26 @@
+import { electronAPI } from '@electron-toolkit/preload';
+import { contextBridge } from 'electron';
+
+import { ElectronDialog } from '../../main/dialog';
+import { SplashScreen } from '../../main/splash-screen';
+
+// Use `contextBridge` APIs to expose Electron APIs to
+// renderer only if context isolation is enabled, otherwise
+// just add to the DOM global.
+if (process.contextIsolated) {
+ try {
+ contextBridge.exposeInMainWorld('electron', electronAPI);
+ contextBridge.exposeInMainWorld('splashScreen', SplashScreen);
+ contextBridge.exposeInMainWorld('electronDialog', ElectronDialog);
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error(error);
+ }
+} else {
+ // @ts-ignore (define in dts)
+ window.electron = electronAPI;
+ // @ts-ignore (define in dts)
+ window.splashScreen = SplashScreen;
+ // @ts-ignore (define in dts)
+ window.electronDialog = ElectronDialog;
+}
diff --git a/src/typings.d.ts b/src/typings.d.ts
deleted file mode 100644
index 14ab332d..00000000
--- a/src/typings.d.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { GrpcClient } from './main/clients/grpc-client/preload';
-import { GrpcWebClient } from './main/clients/grpc-web-client/preload';
-import { ElectronDialog } from './main/dialog/preload';
-import { ElectronStore } from './main/electron-store/preload';
-import { OS } from './main/os/preload';
-import { Protobuf } from './main/protobuf/preload';
-
-declare global {
- interface Window {
- electronStore: typeof ElectronStore;
- electronDialog: typeof ElectronDialog;
- protobuf: typeof Protobuf;
- clients: {
- grpc: typeof GrpcClient;
- grpcWeb: typeof GrpcWebClient;
- };
- os: typeof OS;
- }
-}
diff --git a/src/ui/app/api/index.ts b/src/ui/app/api/index.ts
new file mode 100644
index 00000000..0646cef7
--- /dev/null
+++ b/src/ui/app/api/index.ts
@@ -0,0 +1 @@
+export * from './local';
diff --git a/src/ui/app/api/local/environments.ts b/src/ui/app/api/local/environments.ts
new file mode 100644
index 00000000..0353c1d4
--- /dev/null
+++ b/src/ui/app/api/local/environments.ts
@@ -0,0 +1,19 @@
+import { SetOptional } from 'type-fest';
+
+import { Environment, IEnvironment } from '@core';
+
+export async function fetch() {
+ const data = await window.database.environment.find({});
+
+ const environments = data.map((item) => new Environment(item));
+
+ return environments;
+}
+
+export function upsert(environment: SetOptional) {
+ return window.database.environment.upsert(environment);
+}
+
+export function remove(id: string) {
+ return window.database.environment.delete({ id });
+}
diff --git a/src/ui/app/api/local/index.ts b/src/ui/app/api/local/index.ts
new file mode 100644
index 00000000..48d7fbfd
--- /dev/null
+++ b/src/ui/app/api/local/index.ts
@@ -0,0 +1,11 @@
+import * as environments from './environments';
+import * as settings from './settings';
+import * as tabs from './tabs';
+import * as tlsPresets from './tls-presets';
+
+export const LocalAPI = {
+ environments,
+ settings,
+ tlsPresets,
+ tabs,
+};
diff --git a/src/ui/app/api/local/settings.ts b/src/ui/app/api/local/settings.ts
new file mode 100644
index 00000000..b9934089
--- /dev/null
+++ b/src/ui/app/api/local/settings.ts
@@ -0,0 +1,77 @@
+import {
+ Alignment,
+ isAlignmentSetting,
+ isLanguageSetting,
+ isMenuOptionsSetting,
+ isThemeSetting,
+ Language,
+ MenuOptions,
+ Setting,
+ SettingKey,
+ Theme,
+} from '@core';
+
+export async function fetchTheme() {
+ const row = await window.database.settings.findOneOrFail({ key: SettingKey.THEME });
+
+ if (isThemeSetting(row)) {
+ return new Setting({ key: SettingKey.THEME, value: row.value });
+ }
+
+ throw new Error('Error while fetching settings:theme value.');
+}
+
+export async function setTheme(theme: Theme) {
+ await window.database.settings.upsert({ key: SettingKey.THEME, value: { theme } });
+}
+
+export async function fetchAlignment() {
+ const row = await window.database.settings.findOneOrFail({ key: SettingKey.ALIGNMENT });
+
+ if (isAlignmentSetting(row)) {
+ return new Setting({ key: SettingKey.ALIGNMENT, value: row.value });
+ }
+
+ throw new Error('Error while fetching settings:alignment value.');
+}
+
+export async function setAlignment(alignment: Alignment) {
+ await window.database.settings.upsert({ key: SettingKey.ALIGNMENT, value: { alignment } });
+}
+
+export async function fetchLanguage() {
+ const row = await window.database.settings.findOneOrFail({ key: SettingKey.LANGUAGE });
+
+ if (isLanguageSetting(row)) {
+ return new Setting({ key: SettingKey.LANGUAGE, value: row.value });
+ }
+
+ throw new Error('Error while fetching settings:language value.');
+}
+
+export async function setLanguage(language: Language) {
+ await window.database.settings.upsert({ key: SettingKey.LANGUAGE, value: { language } });
+}
+
+export async function fetchMenuOptions() {
+ const row = await window.database.settings.findOneOrFail({ key: SettingKey.MENU });
+
+ if (isMenuOptionsSetting(row)) {
+ return new Setting({ key: SettingKey.MENU, value: row.value });
+ }
+
+ throw new Error('Error while fetching settings:menu value.');
+}
+
+export async function setMenu(menu: MenuOptions) {
+ await window.database.settings.upsert({ key: SettingKey.MENU, value: menu });
+}
+
+export function fetchAllSettings() {
+ return Promise.all([
+ fetchTheme().then((setting) => setting.value),
+ fetchAlignment().then((setting) => setting.value),
+ fetchLanguage().then((setting) => setting.value),
+ fetchMenuOptions().then((setting) => setting.value),
+ ]);
+}
diff --git a/src/ui/app/api/local/tabs.ts b/src/ui/app/api/local/tabs.ts
new file mode 100644
index 00000000..4cc73c59
--- /dev/null
+++ b/src/ui/app/api/local/tabs.ts
@@ -0,0 +1,15 @@
+import { SetOptional } from 'type-fest';
+
+import { IAbstractTab } from '@core';
+
+export function fetch() {
+ return window.database.tabs.find({});
+}
+
+export function upsert(tab: SetOptional) {
+ return window.database.tabs.upsert(tab);
+}
+
+export function remove(id: string) {
+ return window.database.tabs.delete({ id });
+}
diff --git a/src/ui/app/api/local/tls-presets.ts b/src/ui/app/api/local/tls-presets.ts
new file mode 100644
index 00000000..d26d127b
--- /dev/null
+++ b/src/ui/app/api/local/tls-presets.ts
@@ -0,0 +1,15 @@
+import { SetOptional } from 'type-fest';
+
+import { ITlsPreset } from '@core';
+
+export function fetch() {
+ return window.database.tlsPresets.find({});
+}
+
+export function upsert(tlsPreset: SetOptional) {
+ return window.database.tlsPresets.upsert(tlsPreset);
+}
+
+export function remove(id: string) {
+ return window.database.tlsPresets.delete({ id });
+}
diff --git a/src/app/app.tsx b/src/ui/app/app.tsx
similarity index 56%
rename from src/app/app.tsx
rename to src/ui/app/app.tsx
index 674ef4a3..ec1f9d95 100644
--- a/src/app/app.tsx
+++ b/src/ui/app/app.tsx
@@ -2,18 +2,24 @@ import { NextUIProvider } from '@nextui-org/react';
import React from 'react';
import { NotificationContainer } from '@components';
-import { ThemeType, useSettingsStore } from '@storage';
+import { Theme } from '@core';
+import { useAppStorage } from '@new-storage';
+import { DarkTheme, LightTheme } from '@themes';
import { Main } from './pages';
-import { DarkTheme, globalStyles, LightTheme } from './themes';
+import { globalStyles } from './styles';
export const THEMES = {
- [ThemeType.DARK]: DarkTheme,
- [ThemeType.LIGHT]: LightTheme,
+ [Theme.DARK]: DarkTheme,
+ [Theme.LIGHT]: LightTheme,
};
function App(): JSX.Element {
- const theme = useSettingsStore((store) => store.theme);
+ const { theme, fetch } = useAppStorage((store) => store);
+
+ React.useEffect(() => {
+ fetch();
+ }, []);
globalStyles();
diff --git a/src/app/context/app.context.ts b/src/ui/app/context/app.context.ts
similarity index 100%
rename from src/app/context/app.context.ts
rename to src/ui/app/context/app.context.ts
diff --git a/src/app/context/index.ts b/src/ui/app/context/index.ts
similarity index 100%
rename from src/app/context/index.ts
rename to src/ui/app/context/index.ts
diff --git a/src/ui/app/env.d.ts b/src/ui/app/env.d.ts
new file mode 100644
index 00000000..fe15b4fa
--- /dev/null
+++ b/src/ui/app/env.d.ts
@@ -0,0 +1,3 @@
+///
+
+declare const APP_VERSION: string;
diff --git a/src/ui/app/hooks/clients/grpc/index.ts b/src/ui/app/hooks/clients/grpc/index.ts
new file mode 100644
index 00000000..cb91c1ee
--- /dev/null
+++ b/src/ui/app/hooks/clients/grpc/index.ts
@@ -0,0 +1 @@
+export * from './use-unary-call';
diff --git a/src/ui/app/hooks/clients/grpc/use-unary-call.ts b/src/ui/app/hooks/clients/grpc/use-unary-call.ts
new file mode 100644
index 00000000..74c55e05
--- /dev/null
+++ b/src/ui/app/hooks/clients/grpc/use-unary-call.ts
@@ -0,0 +1 @@
+export function useUnaryCall() {}
diff --git a/src/app/pages/tabs-container/collection-types/index.ts b/src/ui/app/hooks/clients/index.ts
similarity index 100%
rename from src/app/pages/tabs-container/collection-types/index.ts
rename to src/ui/app/hooks/clients/index.ts
diff --git a/src/app/hooks/collections/index.ts b/src/ui/app/hooks/collections/index.ts
similarity index 100%
rename from src/app/hooks/collections/index.ts
rename to src/ui/app/hooks/collections/index.ts
diff --git a/src/app/hooks/collections/use-create-collection.ts b/src/ui/app/hooks/collections/use-create-collection.ts
similarity index 100%
rename from src/app/hooks/collections/use-create-collection.ts
rename to src/ui/app/hooks/collections/use-create-collection.ts
diff --git a/src/app/hooks/collections/use-update-collection.ts b/src/ui/app/hooks/collections/use-update-collection.ts
similarity index 100%
rename from src/app/hooks/collections/use-update-collection.ts
rename to src/ui/app/hooks/collections/use-update-collection.ts
diff --git a/src/app/hooks/index.ts b/src/ui/app/hooks/index.ts
similarity index 79%
rename from src/app/hooks/index.ts
rename to src/ui/app/hooks/index.ts
index 16250580..cd7b685f 100644
--- a/src/app/hooks/index.ts
+++ b/src/ui/app/hooks/index.ts
@@ -1,3 +1,4 @@
export * from './collections';
export * from './protocols';
export * from './use-shortcuts';
+export * from './tabs';
diff --git a/src/app/hooks/protocols/grpc/index.ts b/src/ui/app/hooks/protocols/grpc/index.ts
similarity index 80%
rename from src/app/hooks/protocols/grpc/index.ts
rename to src/ui/app/hooks/protocols/grpc/index.ts
index f334e78a..6eca85ee 100644
--- a/src/app/hooks/protocols/grpc/index.ts
+++ b/src/ui/app/hooks/protocols/grpc/index.ts
@@ -2,4 +2,3 @@ export * from './use-server-streaming';
export * from './use-unary-call';
export * from './use-client-streaming';
export * from './use-bidirectional-streaming';
-export * from './use-grpc-tab-context';
diff --git a/src/app/hooks/protocols/grpc/prepare-request.ts b/src/ui/app/hooks/protocols/grpc/prepare-request.ts
similarity index 95%
rename from src/app/hooks/protocols/grpc/prepare-request.ts
rename to src/ui/app/hooks/protocols/grpc/prepare-request.ts
index d7857eb1..3adc17b4 100644
--- a/src/app/hooks/protocols/grpc/prepare-request.ts
+++ b/src/ui/app/hooks/protocols/grpc/prepare-request.ts
@@ -4,8 +4,9 @@ import {
GrpcOptions,
GrpcTlsConfig,
GrpcTlsType,
-} from '@core/types';
-import { Collection, CollectionType, GrpcTab, TlsPreset } from '@storage';
+ TlsPreset,
+} from '@core';
+import { Collection, CollectionType, GrpcTab } from '@storage';
function getRequestAddress(tab: GrpcTab): string {
if (tab.data.url && tab.data.url.length > 0) {
diff --git a/src/app/hooks/protocols/grpc/use-bidirectional-streaming.ts b/src/ui/app/hooks/protocols/grpc/use-bidirectional-streaming.ts
similarity index 93%
rename from src/app/hooks/protocols/grpc/use-bidirectional-streaming.ts
rename to src/ui/app/hooks/protocols/grpc/use-bidirectional-streaming.ts
index 622fc451..f3b2b104 100644
--- a/src/app/hooks/protocols/grpc/use-bidirectional-streaming.ts
+++ b/src/ui/app/hooks/protocols/grpc/use-bidirectional-streaming.ts
@@ -1,20 +1,15 @@
import { notification } from '@components';
-import { GrpcMethodType } from '@core/types';
-import {
- GrpcStreamMessageType,
- GrpcTab,
- useCollectionsStore,
- useTabsStore,
- useTlsPresetsStore,
-} from '@storage';
+import { GrpcMethodType } from '@core';
+import { useGrpcTabContextStore } from '@hooks';
+import { useAppStorage } from '@new-storage';
+import { GrpcStreamMessageType, GrpcTab, useCollectionsStore, useTabsStore } from '@storage';
import { getOptions, getTlsOptions, parseMetadata, parseRequest } from './prepare-request';
-import { useGrpcTabContextStore } from './use-grpc-tab-context';
export function useBidirectionalStreaming() {
const collections = useCollectionsStore((store) => store.collections);
const { addGrpcStreamMessage } = useTabsStore((store) => store);
- const tlsPresets = useTlsPresetsStore((store) => store.presets);
+ const tlsPresets = useAppStorage((store) => store.tlsPresets);
const { setContext, getContext, updateContext, deleteContext } = useGrpcTabContextStore();
function isRequestLoading(tab: GrpcTab) {
diff --git a/src/app/hooks/protocols/grpc/use-client-streaming.ts b/src/ui/app/hooks/protocols/grpc/use-client-streaming.ts
similarity index 92%
rename from src/app/hooks/protocols/grpc/use-client-streaming.ts
rename to src/ui/app/hooks/protocols/grpc/use-client-streaming.ts
index 18ffd53e..7193c91d 100644
--- a/src/app/hooks/protocols/grpc/use-client-streaming.ts
+++ b/src/ui/app/hooks/protocols/grpc/use-client-streaming.ts
@@ -1,20 +1,15 @@
import { notification } from '@components';
-import { GrpcMethodType } from '@core/types';
-import {
- GrpcStreamMessageType,
- GrpcTab,
- useCollectionsStore,
- useTabsStore,
- useTlsPresetsStore,
-} from '@storage';
+import { GrpcMethodType } from '@core';
+import { useGrpcTabContextStore } from '@hooks';
+import { useAppStorage } from '@new-storage';
+import { GrpcStreamMessageType, GrpcTab, useCollectionsStore, useTabsStore } from '@storage';
import { getOptions, getTlsOptions, parseMetadata, parseRequest } from './prepare-request';
-import { useGrpcTabContextStore } from './use-grpc-tab-context';
export function useClientStreaming() {
const collections = useCollectionsStore((store) => store.collections);
const { addGrpcStreamMessage } = useTabsStore((store) => store);
- const tlsPresets = useTlsPresetsStore((store) => store.presets);
+ const tlsPresets = useAppStorage((store) => store.tlsPresets);
const { setContext, getContext, updateContext, deleteContext } = useGrpcTabContextStore();
function isRequestLoading(tab: GrpcTab) {
diff --git a/src/app/hooks/protocols/grpc/use-server-streaming.ts b/src/ui/app/hooks/protocols/grpc/use-server-streaming.ts
similarity index 94%
rename from src/app/hooks/protocols/grpc/use-server-streaming.ts
rename to src/ui/app/hooks/protocols/grpc/use-server-streaming.ts
index 7399ffc0..ecb611de 100644
--- a/src/app/hooks/protocols/grpc/use-server-streaming.ts
+++ b/src/ui/app/hooks/protocols/grpc/use-server-streaming.ts
@@ -1,21 +1,21 @@
import { notification } from '@components';
-import { GrpcMethodType } from '@core/types';
+import { GrpcMethodType } from '@core';
+import { useGrpcTabContextStore } from '@hooks';
+import { useAppStorage } from '@new-storage';
import {
GrpcProtocol,
GrpcStreamMessageType,
GrpcTab,
useCollectionsStore,
useTabsStore,
- useTlsPresetsStore,
} from '@storage';
import { getOptions, getTlsOptions, parseMetadata, parseRequest } from './prepare-request';
-import { useGrpcTabContextStore } from './use-grpc-tab-context';
export function useServerStreaming() {
const collections = useCollectionsStore((store) => store.collections);
const { addGrpcStreamMessage } = useTabsStore((store) => store);
- const tlsPresets = useTlsPresetsStore((store) => store.presets);
+ const tlsPresets = useAppStorage((store) => store.tlsPresets);
const { setContext, getContext, updateContext, deleteContext } = useGrpcTabContextStore();
function getClient(tab: GrpcTab) {
diff --git a/src/app/hooks/protocols/grpc/use-unary-call.ts b/src/ui/app/hooks/protocols/grpc/use-unary-call.ts
similarity index 86%
rename from src/app/hooks/protocols/grpc/use-unary-call.ts
rename to src/ui/app/hooks/protocols/grpc/use-unary-call.ts
index 462d41c0..81ee801a 100644
--- a/src/app/hooks/protocols/grpc/use-unary-call.ts
+++ b/src/ui/app/hooks/protocols/grpc/use-unary-call.ts
@@ -1,20 +1,15 @@
import { notification } from '@components';
-import { GrpcMethodType } from '@core/types';
-import {
- GrpcProtocol,
- GrpcTab,
- useCollectionsStore,
- useTabsStore,
- useTlsPresetsStore,
-} from '@storage';
+import { GrpcMethodType } from '@core';
+import { useGrpcTabContextStore } from '@hooks';
+import { useAppStorage } from '@new-storage';
+import { GrpcProtocol, GrpcTab, useCollectionsStore, useTabsStore } from '@storage';
import { getOptions, getTlsOptions, parseMetadata, parseRequest } from './prepare-request';
-import { useGrpcTabContextStore } from './use-grpc-tab-context';
export function useUnaryCall() {
const collections = useCollectionsStore((store) => store.collections);
const { updateGrpcTabData } = useTabsStore((store) => store);
- const tlsPresets = useTlsPresetsStore((store) => store.presets);
+ const tlsPresets = useAppStorage((store) => store.tlsPresets);
const { setContext, getContext, deleteContext } = useGrpcTabContextStore();
function getClient(tab: GrpcTab) {
diff --git a/src/ui/app/hooks/protocols/index.ts b/src/ui/app/hooks/protocols/index.ts
new file mode 100644
index 00000000..22d94d3d
--- /dev/null
+++ b/src/ui/app/hooks/protocols/index.ts
@@ -0,0 +1 @@
+export * from './grpc';
diff --git a/src/ui/app/hooks/tabs/index.ts b/src/ui/app/hooks/tabs/index.ts
new file mode 100644
index 00000000..7b2925e8
--- /dev/null
+++ b/src/ui/app/hooks/tabs/index.ts
@@ -0,0 +1 @@
+export * from './use-grpc-tab-context';
diff --git a/src/app/hooks/protocols/grpc/use-grpc-tab-context.ts b/src/ui/app/hooks/tabs/use-grpc-tab-context.ts
similarity index 96%
rename from src/app/hooks/protocols/grpc/use-grpc-tab-context.ts
rename to src/ui/app/hooks/tabs/use-grpc-tab-context.ts
index b9f6a85e..883dd910 100644
--- a/src/app/hooks/protocols/grpc/use-grpc-tab-context.ts
+++ b/src/ui/app/hooks/tabs/use-grpc-tab-context.ts
@@ -1,7 +1,7 @@
import { produce } from 'immer';
-import create from 'zustand';
+import { create } from 'zustand';
-import { GrpcMethodType } from '@core/types';
+import { GrpcMethodType } from '@core';
export type GrpcBaseTabContext = {
callId?: string;
diff --git a/src/app/hooks/use-shortcuts.ts b/src/ui/app/hooks/use-shortcuts.ts
similarity index 100%
rename from src/app/hooks/use-shortcuts.ts
rename to src/ui/app/hooks/use-shortcuts.ts
diff --git a/src/ui/app/index.html b/src/ui/app/index.html
new file mode 100644
index 00000000..a4eaabca
--- /dev/null
+++ b/src/ui/app/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/layouts/default.tsx b/src/ui/app/layouts/default.tsx
similarity index 100%
rename from src/app/layouts/default.tsx
rename to src/ui/app/layouts/default.tsx
diff --git a/src/app/layouts/index.ts b/src/ui/app/layouts/index.ts
similarity index 100%
rename from src/app/layouts/index.ts
rename to src/ui/app/layouts/index.ts
diff --git a/src/ui/app/new-storage/app.interface.ts b/src/ui/app/new-storage/app.interface.ts
new file mode 100644
index 00000000..04d50d27
--- /dev/null
+++ b/src/ui/app/new-storage/app.interface.ts
@@ -0,0 +1,15 @@
+import { EnvironmentsStorageSlice } from './environments/environments.interface';
+import { SettingsStorageSlice } from './settings/settings.interface';
+import { TabsStorageSlice } from './tabs/tabs.interface';
+import { TlsPresetsStorageSlice } from './tls-presets/tls-presets.interface';
+
+export interface AppStorageSlice {
+ fetch: () => Promise;
+ removeEnvironmentAndResetTabs: (id: string) => Promise;
+}
+
+export type AppStorage = AppStorageSlice &
+ EnvironmentsStorageSlice &
+ TabsStorageSlice &
+ SettingsStorageSlice &
+ TlsPresetsStorageSlice;
diff --git a/src/ui/app/new-storage/app.storage.ts b/src/ui/app/new-storage/app.storage.ts
new file mode 100644
index 00000000..42493380
--- /dev/null
+++ b/src/ui/app/new-storage/app.storage.ts
@@ -0,0 +1,33 @@
+import { create, StateCreator } from 'zustand';
+
+import { AppStorage, AppStorageSlice } from './app.interface';
+import { createEnvironmentsSlice } from './environments';
+import { createSettingsSlice } from './settings';
+import { createTabsSlice } from './tabs';
+import { createTlsPresetsSlice } from './tls-presets';
+
+export const createAppStorageSlice: StateCreator = (
+ ...args
+) => ({
+ fetch: async () => {
+ await Promise.all([
+ createEnvironmentsSlice(...args).fetchEnvironments(),
+ createTabsSlice(...args).fetchTabs(),
+ createSettingsSlice(...args).fetchSettings(),
+ createTlsPresetsSlice(...args).fetchTlsPresets(),
+ ]);
+ },
+
+ removeEnvironmentAndResetTabs: async (id: string) => {
+ await createEnvironmentsSlice(...args).removeEnvironment(id);
+ await createTabsSlice(...args).resetEnvironment(id);
+ },
+});
+
+export const useAppStorage = create()((...args) => ({
+ ...createAppStorageSlice(...args),
+ ...createEnvironmentsSlice(...args),
+ ...createTabsSlice(...args),
+ ...createSettingsSlice(...args),
+ ...createTlsPresetsSlice(...args),
+}));
diff --git a/src/ui/app/new-storage/environments/environments.interface.ts b/src/ui/app/new-storage/environments/environments.interface.ts
new file mode 100644
index 00000000..42e97ebd
--- /dev/null
+++ b/src/ui/app/new-storage/environments/environments.interface.ts
@@ -0,0 +1,9 @@
+import { Environment, IEnvironment } from '@core';
+
+export interface EnvironmentsStorageSlice {
+ environments: Environment[];
+
+ fetchEnvironments: () => Promise;
+ createEnvironment: (environment: IEnvironment) => Promise;
+ removeEnvironment: (id: string) => Promise;
+}
diff --git a/src/ui/app/new-storage/environments/environments.storage.ts b/src/ui/app/new-storage/environments/environments.storage.ts
new file mode 100644
index 00000000..9ea47fcd
--- /dev/null
+++ b/src/ui/app/new-storage/environments/environments.storage.ts
@@ -0,0 +1,47 @@
+/* eslint-disable no-param-reassign */
+
+import { produce } from 'immer';
+import { StateCreator } from 'zustand';
+
+import { LocalAPI } from '@api';
+
+import { AppStorage } from '../app.interface';
+import { EnvironmentsStorageSlice } from './environments.interface';
+
+export const createEnvironmentsSlice: StateCreator = (
+ set
+) => ({
+ environments: [],
+
+ fetchEnvironments: async () => {
+ const environments = await LocalAPI.environments.fetch();
+
+ set(
+ produce((state) => {
+ state.environments = environments;
+ })
+ );
+ },
+
+ createEnvironment: async (environment) => {
+ await LocalAPI.environments.upsert(environment);
+ const environments = await LocalAPI.environments.fetch();
+
+ set(
+ produce((state) => {
+ state.environments = environments;
+ })
+ );
+ },
+
+ removeEnvironment: async (id) => {
+ await LocalAPI.environments.remove(id);
+ const environments = await LocalAPI.environments.fetch();
+
+ set(
+ produce((state) => {
+ state.environments = environments;
+ })
+ );
+ },
+});
diff --git a/src/ui/app/new-storage/environments/index.ts b/src/ui/app/new-storage/environments/index.ts
new file mode 100644
index 00000000..087b0886
--- /dev/null
+++ b/src/ui/app/new-storage/environments/index.ts
@@ -0,0 +1,2 @@
+export * from './environments.storage';
+export * from './environments.interface';
diff --git a/src/ui/app/new-storage/index.ts b/src/ui/app/new-storage/index.ts
new file mode 100644
index 00000000..d82e0122
--- /dev/null
+++ b/src/ui/app/new-storage/index.ts
@@ -0,0 +1,5 @@
+export * from './settings';
+export * from './environments';
+export * from './tls-presets';
+export * from './tabs';
+export * from './app.storage';
diff --git a/src/ui/app/new-storage/settings/index.ts b/src/ui/app/new-storage/settings/index.ts
new file mode 100644
index 00000000..3f285fd6
--- /dev/null
+++ b/src/ui/app/new-storage/settings/index.ts
@@ -0,0 +1,2 @@
+export * from './settings.storage';
+export * from './settings.interface';
diff --git a/src/ui/app/new-storage/settings/settings.interface.ts b/src/ui/app/new-storage/settings/settings.interface.ts
new file mode 100644
index 00000000..aeec8ce5
--- /dev/null
+++ b/src/ui/app/new-storage/settings/settings.interface.ts
@@ -0,0 +1,15 @@
+import { Alignment, Language, MenuOptions, Theme } from '@core';
+
+export interface SettingsState {
+ theme: Theme;
+ language: Language;
+ alignment: Alignment;
+ menu: MenuOptions;
+}
+
+export interface SettingsStorageSlice extends SettingsState {
+ fetchSettings: () => Promise;
+ setTheme: (theme: Theme) => Promise;
+ setAlignment: (alignment: Alignment) => Promise;
+ setMenu: (menu: MenuOptions) => Promise;
+}
diff --git a/src/ui/app/new-storage/settings/settings.storage.ts b/src/ui/app/new-storage/settings/settings.storage.ts
new file mode 100644
index 00000000..219ca337
--- /dev/null
+++ b/src/ui/app/new-storage/settings/settings.storage.ts
@@ -0,0 +1,68 @@
+/* eslint-disable no-param-reassign */
+
+import { produce } from 'immer';
+import { StateCreator } from 'zustand';
+
+import { LocalAPI } from '@api';
+import { Alignment, Language, Theme } from '@core';
+
+import { AppStorage } from '../app.interface';
+import { SettingsState, SettingsStorageSlice } from './settings.interface';
+
+const initialState: SettingsState = {
+ theme: Theme.DARK,
+ alignment: Alignment.HORIZONTAL,
+ language: Language.EN,
+ menu: {
+ collapsed: true,
+ },
+};
+
+export const createSettingsSlice: StateCreator = (
+ set
+) => ({
+ ...initialState,
+
+ fetchSettings: async () => {
+ const [theme, alignment, language, menu] = await LocalAPI.settings.fetchAllSettings();
+
+ set(
+ produce((state) => {
+ state.theme = theme;
+ state.alignment = alignment;
+ state.language = language;
+ state.menu = menu;
+ })
+ );
+ },
+
+ setTheme: async (theme) => {
+ await LocalAPI.settings.setTheme(theme);
+
+ set(
+ produce((state) => {
+ state.theme = theme;
+ })
+ );
+ },
+
+ setAlignment: async (alignment) => {
+ await LocalAPI.settings.setAlignment(alignment);
+
+ set(
+ produce((state) => {
+ state.alignment = alignment;
+ })
+ );
+ },
+
+ setMenu: async (menu) => {
+ await LocalAPI.settings.setMenu(menu);
+
+ set(
+ produce((state) => {
+ state.menu = menu;
+ })
+ );
+ },
+});
diff --git a/src/ui/app/new-storage/tabs/index.ts b/src/ui/app/new-storage/tabs/index.ts
new file mode 100644
index 00000000..3d8e1fcb
--- /dev/null
+++ b/src/ui/app/new-storage/tabs/index.ts
@@ -0,0 +1,2 @@
+export * from './tabs.interface';
+export * from './tabs.storage';
diff --git a/src/ui/app/new-storage/tabs/tabs.interface.ts b/src/ui/app/new-storage/tabs/tabs.interface.ts
new file mode 100644
index 00000000..c182f8a9
--- /dev/null
+++ b/src/ui/app/new-storage/tabs/tabs.interface.ts
@@ -0,0 +1,19 @@
+import { IAbstractTab, ICreateTabPayload, IUpdateTabPayload } from '@core';
+
+export interface TabsState {
+ tabs: IAbstractTab[];
+
+ activeTabId?: string;
+}
+
+export interface TabsStorageSlice extends TabsState {
+ fetchTabs: () => Promise;
+ createTab: (payload: ICreateTabPayload) => Promise;
+ updateTab: (id: string, payload: IUpdateTabPayload) => Promise;
+ moveTab: (currentId: string, overId: string) => Promise;
+ activateTab: (id: string) => Promise;
+ closeTab: (id: string) => Promise;
+ closeActiveTab: () => Promise;
+ closeAllTabs: () => Promise;
+ resetEnvironment: (environemntId: string) => Promise;
+}
diff --git a/src/ui/app/new-storage/tabs/tabs.storage.ts b/src/ui/app/new-storage/tabs/tabs.storage.ts
new file mode 100644
index 00000000..62cc386a
--- /dev/null
+++ b/src/ui/app/new-storage/tabs/tabs.storage.ts
@@ -0,0 +1,122 @@
+/* eslint-disable no-param-reassign */
+
+import { produce } from 'immer';
+import { StateCreator } from 'zustand';
+
+import { LocalAPI } from '@api';
+import { TabsContainer } from '@core';
+
+import { AppStorage } from '../app.interface';
+import { TabsState, TabsStorageSlice } from './tabs.interface';
+
+let container = new TabsContainer();
+
+const initialState: TabsState = {
+ tabs: [...container.getTabs()],
+};
+
+export const createTabsSlice: StateCreator = (set) => ({
+ ...initialState,
+
+ fetchTabs: async () => {
+ const tabs = await LocalAPI.tabs.fetch();
+
+ container = new TabsContainer(tabs);
+
+ set(
+ produce((state) => {
+ state.tabs = [...container.getTabs()];
+ })
+ );
+ },
+
+ createTab: async (payload) => {
+ container.createTab(payload);
+ // const tab = container.create(payload);
+
+ // await LocalAPI.tabs.upsert(tab);
+ // const tabs = await LocalAPI.tabs.fetch();
+
+ // container = new TabsContainer(tabs);
+ set(
+ produce((state) => {
+ state.tabs = [...container.getTabs()];
+ state.activeTabId = container.getActiveTab()?.id;
+ })
+ );
+ },
+
+ updateTab: async (id, payload) => {
+ container.updateTabs([id], payload);
+
+ set(
+ produce