gRPC in import/export

This commit is contained in:
Gregory Schier
2024-02-23 16:16:13 -08:00
parent 4571d7dc93
commit 3dfb8b3fb2
10 changed files with 137 additions and 132 deletions

View File

@@ -10,9 +10,21 @@ export function pluginHookImport(contents) {
return undefined; return undefined;
} }
if (parsed.yaakSchema !== 1) return undefined; if (!('yaakSchema' in parsed)) {
return;
}
return { resources: parsed.resources }; // Should already be in the correct format // Migrate v1 to v2 -- changes requests to httpRequests
if (parsed.yaakSchema === 1) {
parsed.resources.httpRequests = parsed.resources.requests;
parsed.yaakSchema = 2;
}
if (parsed.yaakSchema === 2) {
return { resources: parsed.resources }; // Should already be in the correct format
}
return undefined;
} }
export function isJSObject(obj) { export function isJSObject(obj) {

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO grpc_events (\n id, workspace_id, request_id, connection_id, content, event_type, metadata,\n status, error\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n content = excluded.content,\n event_type = excluded.event_type,\n metadata = excluded.metadata,\n status = excluded.status,\n error = excluded.error\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 9
},
"nullable": []
},
"hash": "14930955e8a914e292dfbebfce2ea43cc41c1d517386ed816c16d436bf626bf3"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO grpc_events (\n id, workspace_id, request_id, connection_id, content, event_type, metadata, \n status, error\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n content = excluded.content,\n event_type = excluded.event_type,\n metadata = excluded.metadata,\n status = excluded.status,\n error = excluded.error\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 9
},
"nullable": []
},
"hash": "df70bef8eac244eeedd03f5e42573b4c9dbd19cd764e97817c7de30209d21af9"
}

View File

@@ -42,22 +42,8 @@ use window_ext::TrafficLightWindowExt;
use crate::analytics::{AnalyticsAction, AnalyticsResource}; use crate::analytics::{AnalyticsAction, AnalyticsResource};
use crate::grpc::metadata_to_map; use crate::grpc::metadata_to_map;
use crate::http::send_http_request; use crate::http::send_http_request;
use crate::models::{ use crate::models::{cancel_pending_grpc_connections, cancel_pending_responses, create_http_response, delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar, delete_environment, delete_folder, delete_grpc_connection, delete_grpc_request, delete_http_request, delete_http_response, delete_workspace, duplicate_grpc_request, duplicate_http_request, get_cookie_jar, get_environment, get_folder, get_grpc_connection, get_grpc_request, get_http_request, get_http_response, get_key_value_raw, get_or_create_settings, get_workspace, get_workspace_export_resources, list_cookie_jars, list_environments, list_folders, list_grpc_connections, list_grpc_events, list_grpc_requests, list_requests, list_responses, list_workspaces, set_key_value_raw, update_response_if_id, update_settings, upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection, upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_workspace, CookieJar, Environment, EnvironmentVariable, Folder, GrpcConnection, GrpcEvent, GrpcEventType, GrpcRequest, HttpRequest, HttpResponse, KeyValue, Settings, Workspace, WorkspaceExportResources};
cancel_pending_grpc_connections, cancel_pending_responses, create_http_response, use crate::plugin::{ImportResult};
delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar, delete_environment,
delete_folder, delete_grpc_connection, delete_grpc_request, delete_http_request,
delete_http_response, delete_workspace, duplicate_grpc_request, duplicate_http_request,
get_cookie_jar, get_environment, get_folder, get_grpc_connection, get_grpc_request,
get_http_request, get_http_response, get_key_value_raw, get_or_create_settings, get_workspace,
get_workspace_export_resources, list_cookie_jars, list_environments, list_folders,
list_grpc_connections, list_grpc_events, list_grpc_requests, list_requests, list_responses,
list_workspaces, set_key_value_raw, update_response_if_id, update_settings, upsert_cookie_jar,
upsert_environment, upsert_folder, upsert_grpc_connection, upsert_grpc_event,
upsert_grpc_request, upsert_http_request, upsert_workspace, CookieJar, Environment,
EnvironmentVariable, Folder, GrpcConnection, GrpcEvent, GrpcEventType, GrpcRequest,
HttpRequest, HttpResponse, KeyValue, Settings, Workspace,
};
use crate::plugin::{ImportResources, ImportResult};
use crate::updates::{update_mode_from_str, UpdateMode, YaakUpdater}; use crate::updates::{update_mode_from_str, UpdateMode, YaakUpdater};
mod analytics; mod analytics;
@@ -691,7 +677,7 @@ async fn cmd_filter_response(w: Window, response_id: &str, filter: &str) -> Resu
} }
#[tauri::command] #[tauri::command]
async fn cmd_import_data(w: Window, file_paths: Vec<&str>) -> Result<ImportResources, String> { async fn cmd_import_data(w: Window, file_paths: Vec<&str>) -> Result<WorkspaceExportResources, String> {
let mut result: Option<ImportResult> = None; let mut result: Option<ImportResult> = None;
let plugins = vec!["importer-yaak", "importer-insomnia", "importer-postman"]; let plugins = vec!["importer-yaak", "importer-insomnia", "importer-postman"];
for plugin_name in plugins { for plugin_name in plugins {
@@ -714,7 +700,7 @@ async fn cmd_import_data(w: Window, file_paths: Vec<&str>) -> Result<ImportResou
match result { match result {
None => Err("No importers found for the chosen file".to_string()), None => Err("No importers found for the chosen file".to_string()),
Some(r) => { Some(r) => {
let mut imported_resources = ImportResources::default(); let mut imported_resources = WorkspaceExportResources::default();
info!("Importing resources"); info!("Importing resources");
for v in r.resources.workspaces { for v in r.resources.workspaces {
@@ -739,11 +725,19 @@ async fn cmd_import_data(w: Window, file_paths: Vec<&str>) -> Result<ImportResou
info!("Imported folder: {}", x.name); info!("Imported folder: {}", x.name);
} }
for v in r.resources.requests { for v in r.resources.http_requests {
let x = upsert_http_request(&w, v) let x = upsert_http_request(&w, v)
.await .await
.expect("Failed to create request"); .expect("Failed to create HTTP request");
imported_resources.requests.push(x.clone()); imported_resources.http_requests.push(x.clone());
info!("Imported request: {}", x.name);
}
for v in r.resources.grpc_requests {
let x = upsert_grpc_request(&w, &v)
.await
.expect("Failed to create GRPC request");
imported_resources.grpc_requests.push(x.clone());
info!("Imported request: {}", x.name); info!("Imported request: {}", x.name);
} }

View File

@@ -712,7 +712,7 @@ pub async fn upsert_grpc_event(
sqlx::query!( sqlx::query!(
r#" r#"
INSERT INTO grpc_events ( INSERT INTO grpc_events (
id, workspace_id, request_id, connection_id, content, event_type, metadata, id, workspace_id, request_id, connection_id, content, event_type, metadata,
status, error status, error
) )
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
@@ -743,10 +743,7 @@ pub async fn upsert_grpc_event(
} }
} }
pub async fn get_grpc_event( pub async fn get_grpc_event(mgr: &impl Manager<Wry>, id: &str) -> Result<GrpcEvent, sqlx::Error> {
mgr: &impl Manager<Wry>,
id: &str,
) -> Result<GrpcEvent, sqlx::Error> {
let db = get_db(mgr).await; let db = get_db(mgr).await;
sqlx::query_as!( sqlx::query_as!(
GrpcEvent, GrpcEvent,
@@ -760,8 +757,8 @@ pub async fn get_grpc_event(
"#, "#,
id, id,
) )
.fetch_one(&db) .fetch_one(&db)
.await .await
} }
pub async fn list_grpc_events( pub async fn list_grpc_events(
@@ -781,8 +778,8 @@ pub async fn list_grpc_events(
"#, "#,
connection_id, connection_id,
) )
.fetch_all(&db) .fetch_all(&db)
.await .await
} }
pub async fn upsert_cookie_jar( pub async fn upsert_cookie_jar(
@@ -1532,19 +1529,20 @@ pub fn generate_id(prefix: Option<&str>) -> String {
#[derive(Default, Debug, Deserialize, Serialize)] #[derive(Default, Debug, Deserialize, Serialize)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
pub struct WorkspaceExport { pub struct WorkspaceExport {
yaak_version: String, pub yaak_version: String,
yaak_schema: i64, pub yaak_schema: i64,
timestamp: NaiveDateTime, pub timestamp: NaiveDateTime,
resources: WorkspaceExportResources, pub resources: WorkspaceExportResources,
} }
#[derive(Default, Debug, Deserialize, Serialize)] #[derive(Default, Debug, Deserialize, Serialize)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
pub struct WorkspaceExportResources { pub struct WorkspaceExportResources {
workspaces: Vec<Workspace>, pub workspaces: Vec<Workspace>,
environments: Vec<Environment>, pub environments: Vec<Environment>,
folders: Vec<Folder>, pub folders: Vec<Folder>,
requests: Vec<HttpRequest>, pub http_requests: Vec<HttpRequest>,
pub grpc_requests: Vec<GrpcRequest>,
} }
pub async fn get_workspace_export_resources( pub async fn get_workspace_export_resources(
@@ -1556,7 +1554,7 @@ pub async fn get_workspace_export_resources(
.expect("Failed to get workspace"); .expect("Failed to get workspace");
return WorkspaceExport { return WorkspaceExport {
yaak_version: app_handle.package_info().version.clone().to_string(), yaak_version: app_handle.package_info().version.clone().to_string(),
yaak_schema: 1, yaak_schema: 2,
timestamp: chrono::Utc::now().naive_utc(), timestamp: chrono::Utc::now().naive_utc(),
resources: WorkspaceExportResources { resources: WorkspaceExportResources {
workspaces: vec![workspace], workspaces: vec![workspace],
@@ -1566,9 +1564,12 @@ pub async fn get_workspace_export_resources(
folders: list_folders(app_handle, workspace_id) folders: list_folders(app_handle, workspace_id)
.await .await
.expect("Failed to get folders"), .expect("Failed to get folders"),
requests: list_requests(app_handle, workspace_id) http_requests: list_requests(app_handle, workspace_id)
.await .await
.expect("Failed to get requests"), .expect("Failed to get requests"),
grpc_requests: list_grpc_requests(app_handle, workspace_id)
.await
.expect("Failed to get grpc requests"),
}, },
}; };
} }

View File

@@ -1,18 +1,18 @@
use std::fs; use std::fs;
use boa_engine::{
Context, js_string, JsNativeError, JsValue, Module, module::SimpleModuleLoader,
property::Attribute, Source,
};
use boa_engine::builtins::promise::PromiseState; use boa_engine::builtins::promise::PromiseState;
use boa_engine::module::ModuleLoader; use boa_engine::module::ModuleLoader;
use boa_engine::{
js_string, module::SimpleModuleLoader, property::Attribute, Context, JsNativeError, JsValue,
Module, Source,
};
use boa_runtime::Console; use boa_runtime::Console;
use log::{debug, error}; use log::{debug, error};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
use tauri::AppHandle; use tauri::AppHandle;
use crate::models::{Environment, Folder, HttpRequest, Workspace}; use crate::models::{Environment, Folder, HttpRequest, Workspace, WorkspaceExportResources};
#[derive(Default, Debug, Deserialize, Serialize)] #[derive(Default, Debug, Deserialize, Serialize)]
pub struct FilterResult { pub struct FilterResult {
@@ -21,15 +21,7 @@ pub struct FilterResult {
#[derive(Default, Debug, Deserialize, Serialize)] #[derive(Default, Debug, Deserialize, Serialize)]
pub struct ImportResult { pub struct ImportResult {
pub resources: ImportResources, pub resources: WorkspaceExportResources,
}
#[derive(Default, Debug, Deserialize, Serialize)]
pub struct ImportResources {
pub workspaces: Vec<Workspace>,
pub environments: Vec<Environment>,
pub folders: Vec<Folder>,
pub requests: Vec<HttpRequest>,
} }
pub async fn run_plugin_filter( pub async fn run_plugin_filter(

View File

@@ -1,6 +1,6 @@
import classNames from 'classnames'; import classNames from 'classnames';
import { format } from 'date-fns'; import { format } from 'date-fns';
import type { CSSProperties, ReactNode } from 'react'; import type { CSSProperties } from 'react';
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { useGrpcConnections } from '../hooks/useGrpcConnections'; import { useGrpcConnections } from '../hooks/useGrpcConnections';
import { useGrpcEvents } from '../hooks/useGrpcEvents'; import { useGrpcEvents } from '../hooks/useGrpcEvents';

View File

@@ -65,30 +65,7 @@ export function SettingsDropdown() {
key: 'import-data', key: 'import-data',
label: 'Import Data', label: 'Import Data',
leftSlot: <Icon icon="folderInput" />, leftSlot: <Icon icon="folderInput" />,
onSelect: () => { onSelect: () => importData.mutate(),
dialog.show({
id: 'import',
title: 'Import Data',
size: 'sm',
render: ({ hide }) => {
return (
<VStack space={3} className="pb-4">
<p>Insomnia or Postman Collection v2/v2.1 formats are supported</p>
<Button
size="sm"
color="primary"
onClick={async () => {
await importData.mutateAsync();
hide();
}}
>
Select File
</Button>
</VStack>
);
},
});
},
}, },
{ {
key: 'export-data', key: 'export-data',

View File

@@ -9,6 +9,7 @@ import type {
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useWindowSize } from 'react-use'; import { useWindowSize } from 'react-use';
import { useActiveRequest } from '../hooks/useActiveRequest'; import { useActiveRequest } from '../hooks/useActiveRequest';
import { useImportData } from '../hooks/useImportData';
import { useIsFullscreen } from '../hooks/useIsFullscreen'; import { useIsFullscreen } from '../hooks/useIsFullscreen';
import { useOsInfo } from '../hooks/useOsInfo'; import { useOsInfo } from '../hooks/useOsInfo';
import { useSidebarHidden } from '../hooks/useSidebarHidden'; import { useSidebarHidden } from '../hooks/useSidebarHidden';
@@ -36,6 +37,7 @@ export default function Workspace() {
const { hide, show, hidden } = useSidebarHidden(); const { hide, show, hidden } = useSidebarHidden();
const activeRequest = useActiveRequest(); const activeRequest = useActiveRequest();
const windowSize = useWindowSize(); const windowSize = useWindowSize();
const importData = useImportData();
const [floating, setFloating] = useState<boolean>(false); const [floating, setFloating] = useState<boolean>(false);
const [isResizing, setIsResizing] = useState<boolean>(false); const [isResizing, setIsResizing] = useState<boolean>(false);
const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>( const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>(
@@ -165,7 +167,7 @@ export default function Workspace() {
hotkeys={['http_request.create', 'sidebar.toggle', 'settings.show']} hotkeys={['http_request.create', 'sidebar.toggle', 'settings.show']}
bottomSlot={ bottomSlot={
<HStack space={1} justifyContent="center" className="mt-3"> <HStack space={1} justifyContent="center" className="mt-3">
<Button size="sm" color="gray"> <Button size="sm" color="gray" onClick={() => importData.mutate()}>
Import Import
</Button> </Button>
<Button size="sm" color="gray"> <Button size="sm" color="gray">

View File

@@ -5,7 +5,7 @@ import { open } from '@tauri-apps/api/dialog';
import { Button } from '../components/core/Button'; import { Button } from '../components/core/Button';
import { VStack } from '../components/core/Stacks'; import { VStack } from '../components/core/Stacks';
import { useDialog } from '../components/DialogContext'; import { useDialog } from '../components/DialogContext';
import type { Environment, Folder, HttpRequest, Workspace } from '../lib/models'; import type { Environment, Folder, GrpcRequest, HttpRequest, Workspace } from '../lib/models';
import { count } from '../lib/pluralize'; import { count } from '../lib/pluralize';
import { useAlert } from './useAlert'; import { useAlert } from './useAlert';
import { useAppRoutes } from './useAppRoutes'; import { useAppRoutes } from './useAppRoutes';
@@ -20,57 +20,84 @@ export function useImportData() {
const dialog = useDialog(); const dialog = useDialog();
const alert = useAlert(); const alert = useAlert();
const importData = async () => {
const selected = await open(openArgs);
if (selected == null || selected.length === 0) {
return;
}
const imported: {
workspaces: Workspace[];
environments: Environment[];
folders: Folder[];
httpRequests: HttpRequest[];
grpcRequests: GrpcRequest[];
} = await invoke('cmd_import_data', {
filePaths: Array.isArray(selected) ? selected : [selected],
});
const importedWorkspace = imported.workspaces[0];
dialog.show({
id: 'import-complete',
title: 'Import Complete',
size: 'sm',
hideX: true,
render: ({ hide }) => {
const { workspaces, environments, folders, httpRequests, grpcRequests } = imported;
return (
<VStack space={3} className="pb-4">
<ul className="list-disc pl-6">
<li>{count('Workspace', workspaces.length)}</li>
<li>{count('Environment', environments.length)}</li>
<li>{count('Folder', folders.length)}</li>
<li>{count('HTTP Request', httpRequests.length)}</li>
<li>{count('GRPC Request', grpcRequests.length)}</li>
</ul>
<div>
<Button className="ml-auto" onClick={hide} color="primary">
Done
</Button>
</div>
</VStack>
);
},
});
if (importedWorkspace != null) {
routes.navigate('workspace', {
workspaceId: importedWorkspace.id,
environmentId: imported.environments[0]?.id,
});
}
};
return useMutation({ return useMutation({
onError: (err: string) => { onError: (err: string) => {
alert({ id: 'import-failed', title: 'Import Failed', body: err }); alert({ id: 'import-failed', title: 'Import Failed', body: err });
}, },
mutationFn: async () => { mutationFn: async () => {
const selected = await open(openArgs);
if (selected == null || selected.length === 0) {
return;
}
const imported: {
workspaces: Workspace[];
environments: Environment[];
folders: Folder[];
requests: HttpRequest[];
} = await invoke('cmd_import_data', {
filePaths: Array.isArray(selected) ? selected : [selected],
});
const importedWorkspace = imported.workspaces[0];
dialog.show({ dialog.show({
id: 'import-complete', id: 'import',
title: 'Import Complete', title: 'Import Data',
size: 'sm', size: 'sm',
hideX: true,
render: ({ hide }) => { render: ({ hide }) => {
const { workspaces, environments, folders, requests } = imported;
return ( return (
<VStack space={3} className="pb-4"> <VStack space={3} className="pb-4">
<ul className="list-disc pl-6"> <p>Insomnia or Postman Collection v2/v2.1 formats are supported</p>
<li>{count('Workspace', workspaces.length)}</li> <Button
<li>{count('Environment', environments.length)}</li> size="sm"
<li>{count('Folder', folders.length)}</li> color="primary"
<li>{count('Request', requests.length)}</li> onClick={async () => {
</ul> await importData();
<div> hide();
<Button className="ml-auto" onClick={hide} color="primary"> }}
Done >
</Button> Select File
</div> </Button>
</VStack> </VStack>
); );
}, },
}); });
if (importedWorkspace != null) {
routes.navigate('workspace', {
workspaceId: importedWorkspace.id,
environmentId: imported.environments[0]?.id,
});
}
}, },
}); });
} }