mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-08-24 20:34:05 +02:00
Import from a URL in the Import Data dialog (#551)
This commit is contained in:
@@ -1,56 +1,138 @@
|
|||||||
import { VStack } from "@yaakapp-internal/ui";
|
import { platform } from "@yaakapp-internal/platform";
|
||||||
import { useState } from "react";
|
import { Icon, VStack } from "@yaakapp-internal/ui";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useLocalStorage } from "react-use";
|
import { useLocalStorage } from "react-use";
|
||||||
import { CommercialUseBanner } from "./CommercialUseBanner";
|
import { CommercialUseBanner } from "./CommercialUseBanner";
|
||||||
import { Button } from "./core/Button";
|
import { Button } from "./core/Button";
|
||||||
import { SelectFile } from "./SelectFile";
|
import { PlainInput } from "./core/PlainInput";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
importData: (filePath: string) => Promise<void>;
|
importFile: (filePath: string) => Promise<void>;
|
||||||
|
importUrl: (url: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ImportDataDialog({ importData }: Props) {
|
/**
|
||||||
|
* An absolute or relative path is unambiguously a file. Everything else is treated as a URL, so a
|
||||||
|
* bare host like `example.com/openapi.json` still works (the backend defaults it to https).
|
||||||
|
*/
|
||||||
|
function isFilePath(value: string): boolean {
|
||||||
|
return (
|
||||||
|
value.startsWith("/") ||
|
||||||
|
value.startsWith("./") ||
|
||||||
|
value.startsWith("../") ||
|
||||||
|
value.startsWith("~/") ||
|
||||||
|
value.startsWith("\\\\") ||
|
||||||
|
/^[a-zA-Z]:[\\/]/.test(value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileName(path: string): string {
|
||||||
|
return path.split(/[/\\]/).at(-1) || path;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ImportDataDialog({ importFile, importUrl }: Props) {
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
const [filePath, setFilePath] = useLocalStorage<string | null>("importFilePath", null);
|
// A file path or a URL. Both inputs write here, so there is only ever one thing to import
|
||||||
|
const [source, setSource] = useLocalStorage<string | null>("importPathOrUrl", null);
|
||||||
|
const [forceUpdateKey, setForceUpdateKey] = useState<number>(0);
|
||||||
|
const [isHovering, setIsHovering] = useState<boolean>(false);
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const trimmedSource = source?.trim() ?? "";
|
||||||
|
const filePath = isFilePath(trimmedSource) ? trimmedSource : null;
|
||||||
|
|
||||||
|
const selectSource = (value: string) => {
|
||||||
|
setSource(value);
|
||||||
|
// Remount the input so it shows the path of the newly-picked file
|
||||||
|
setForceUpdateKey((k) => k + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Accept a file dropped anywhere on the dialog, the way SelectFile does for its button
|
||||||
|
useEffect(() => {
|
||||||
|
return platform.window.onDragDrop((event) => {
|
||||||
|
if (event.type === "over") {
|
||||||
|
const p = event.position;
|
||||||
|
const r = ref.current?.getBoundingClientRect();
|
||||||
|
if (r == null) return;
|
||||||
|
setIsHovering(p.x >= r.left && p.x <= r.right && p.y >= r.top && p.y <= r.bottom);
|
||||||
|
} else if (event.type === "drop" && isHovering) {
|
||||||
|
const p = event.paths[0];
|
||||||
|
if (p) selectSource(p);
|
||||||
|
setIsHovering(false);
|
||||||
|
} else {
|
||||||
|
setIsHovering(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [isHovering, setSource]);
|
||||||
|
|
||||||
|
const handleSelectFile = async () => {
|
||||||
|
const selected = await platform.dialog.open({ title: "Select File", multiple: false });
|
||||||
|
if (selected == null) return;
|
||||||
|
selectSource(selected);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImport = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
if (filePath != null) {
|
||||||
|
await importFile(filePath);
|
||||||
|
} else {
|
||||||
|
await importUrl(trimmedSource);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack space={5} className="pb-4">
|
<VStack ref={ref} space={4} className="pb-4">
|
||||||
<CommercialUseBanner source="data-import" title="Importing work data?" />
|
<CommercialUseBanner source="data-import" title="Importing work data?" />
|
||||||
|
|
||||||
<VStack space={1}>
|
<button
|
||||||
<ul className="list-disc pl-5">
|
type="button"
|
||||||
<li>OpenAPI 3.0, 3.1</li>
|
onClick={handleSelectFile}
|
||||||
<li>Postman Collection v2, v2.1</li>
|
className={classNames(
|
||||||
<li>Insomnia v4+</li>
|
"w-full rounded-lg border border-dashed px-4 py-6",
|
||||||
<li>Swagger 2.0</li>
|
"flex flex-col items-center gap-1 text-center",
|
||||||
<li>
|
isHovering ? "border-notice bg-surface-highlight" : "border-border hover:border-text",
|
||||||
Curl commands <em className="text-text-subtle">(or paste into URL)</em>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</VStack>
|
|
||||||
<VStack space={2}>
|
|
||||||
<SelectFile
|
|
||||||
filePath={filePath ?? null}
|
|
||||||
onChange={({ filePath }) => setFilePath(filePath)}
|
|
||||||
/>
|
|
||||||
{filePath && (
|
|
||||||
<Button
|
|
||||||
color="primary"
|
|
||||||
disabled={!filePath || isLoading}
|
|
||||||
isLoading={isLoading}
|
|
||||||
size="sm"
|
|
||||||
onClick={async () => {
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
await importData(filePath);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isLoading ? "Importing" : "Import"}
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
|
>
|
||||||
|
<Icon icon="folder_input" className="text-text-subtlest w-8! h-8! mb-2" />
|
||||||
|
{/* Fixed height so the region doesn't resize between the empty and selected states */}
|
||||||
|
<div className="h-6 w-full flex items-center justify-center">
|
||||||
|
{filePath == null ? (
|
||||||
|
<div className="text-text">
|
||||||
|
<strong className="font-semibold">Choose a file</strong> or drag it here
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-text font-mono text-xs max-w-full truncate" title={filePath}>
|
||||||
|
{fileName(filePath)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-text-subtlest">
|
||||||
|
Supports OpenAPI, Swagger, Postman, Insomnia, and curl
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<VStack space={2}>
|
||||||
|
<PlainInput
|
||||||
|
label="Or enter a file path or URL"
|
||||||
|
size="sm"
|
||||||
|
placeholder="https://example.com/openapi.json"
|
||||||
|
defaultValue={source ?? ""}
|
||||||
|
forceUpdateKey={String(forceUpdateKey)}
|
||||||
|
onChange={setSource}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
disabled={trimmedSource === "" || isLoading}
|
||||||
|
isLoading={isLoading}
|
||||||
|
size="sm"
|
||||||
|
onClick={handleImport}
|
||||||
|
>
|
||||||
|
{isLoading ? "Importing" : "Import"}
|
||||||
|
</Button>
|
||||||
</VStack>
|
</VStack>
|
||||||
</VStack>
|
</VStack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,11 +2,9 @@ import type { BatchUpsertResult } from "@yaakapp-internal/models";
|
|||||||
import { FormattedError, VStack } from "@yaakapp-internal/ui";
|
import { FormattedError, VStack } from "@yaakapp-internal/ui";
|
||||||
import { Button } from "../components/core/Button";
|
import { Button } from "../components/core/Button";
|
||||||
import { ImportDataDialog } from "../components/ImportDataDialog";
|
import { ImportDataDialog } from "../components/ImportDataDialog";
|
||||||
import { activeWorkspaceAtom } from "../hooks/useActiveWorkspace";
|
|
||||||
import { createFastMutation } from "../hooks/useFastMutation";
|
import { createFastMutation } from "../hooks/useFastMutation";
|
||||||
import { showAlert } from "./alert";
|
import { showAlert } from "./alert";
|
||||||
import { showDialog } from "./dialog";
|
import { showDialog } from "./dialog";
|
||||||
import { jotaiStore } from "./jotai";
|
|
||||||
import { pluralizeCount } from "./pluralize";
|
import { pluralizeCount } from "./pluralize";
|
||||||
import { router } from "./router";
|
import { router } from "./router";
|
||||||
import { rpc } from "./rpc";
|
import { rpc } from "./rpc";
|
||||||
@@ -28,12 +26,9 @@ export const importData = createFastMutation({
|
|||||||
title: "Import Data",
|
title: "Import Data",
|
||||||
size: "sm",
|
size: "sm",
|
||||||
render: ({ hide }) => {
|
render: ({ hide }) => {
|
||||||
const importAndHide = async (filePath: string) => {
|
const importAndHide = async (runImport: () => Promise<BatchUpsertResult>) => {
|
||||||
try {
|
try {
|
||||||
const didImport = await performImport(filePath);
|
await finishImport(await runImport());
|
||||||
if (!didImport) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve();
|
resolve();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
@@ -41,20 +36,23 @@ export const importData = createFastMutation({
|
|||||||
hide();
|
hide();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return <ImportDataDialog importData={importAndHide} />;
|
return (
|
||||||
|
<ImportDataDialog
|
||||||
|
importFile={(filePath) =>
|
||||||
|
importAndHide(() => rpc<BatchUpsertResult>("cmd_import_data", { filePath }))
|
||||||
|
}
|
||||||
|
importUrl={(url) =>
|
||||||
|
importAndHide(() => rpc<BatchUpsertResult>("cmd_import_url", { url }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
async function performImport(filePath: string): Promise<boolean> {
|
async function finishImport(imported: BatchUpsertResult): Promise<void> {
|
||||||
const activeWorkspace = jotaiStore.get(activeWorkspaceAtom);
|
|
||||||
const imported = await rpc<BatchUpsertResult>("cmd_import_data", {
|
|
||||||
filePath,
|
|
||||||
workspaceId: activeWorkspace?.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const importedWorkspace = imported.workspaces[0];
|
const importedWorkspace = imported.workspaces[0];
|
||||||
|
|
||||||
showDialog({
|
showDialog({
|
||||||
@@ -103,6 +101,4 @@ async function performImport(filePath: string): Promise<boolean> {
|
|||||||
search: { environment_id: environmentId },
|
search: { environment_id: environmentId },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
File diff suppressed because one or more lines are too long
@@ -5,6 +5,7 @@ use std::fs::read_to_string;
|
|||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
use tauri::{Manager, Runtime, WebviewWindow};
|
use tauri::{Manager, Runtime, WebviewWindow};
|
||||||
use yaak::import::{self, ImportDataParams};
|
use yaak::import::{self, ImportDataParams};
|
||||||
|
use yaak_api::{ApiClientKind, yaak_api_client};
|
||||||
use yaak_core::WorkspaceContext;
|
use yaak_core::WorkspaceContext;
|
||||||
use yaak_models::util::BatchUpsertResult;
|
use yaak_models::util::BatchUpsertResult;
|
||||||
use yaak_plugins::manager::PluginManager;
|
use yaak_plugins::manager::PluginManager;
|
||||||
@@ -13,10 +14,25 @@ use yaak_tauri_utils::window::WorkspaceWindowTrait;
|
|||||||
pub(crate) async fn import_data<R: Runtime>(
|
pub(crate) async fn import_data<R: Runtime>(
|
||||||
window: &WebviewWindow<R>,
|
window: &WebviewWindow<R>,
|
||||||
file_path: &str,
|
file_path: &str,
|
||||||
|
) -> Result<BatchUpsertResult> {
|
||||||
|
let contents = read_import_file(file_path)?;
|
||||||
|
import_contents(window, &contents).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn import_url<R: Runtime>(
|
||||||
|
window: &WebviewWindow<R>,
|
||||||
|
url: &str,
|
||||||
|
) -> Result<BatchUpsertResult> {
|
||||||
|
let contents = fetch_import_url(window, url).await?;
|
||||||
|
import_contents(window, &contents).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn import_contents<R: Runtime>(
|
||||||
|
window: &WebviewWindow<R>,
|
||||||
|
contents: &str,
|
||||||
) -> Result<BatchUpsertResult> {
|
) -> Result<BatchUpsertResult> {
|
||||||
let plugin_manager = window.state::<PluginManager>();
|
let plugin_manager = window.state::<PluginManager>();
|
||||||
let query_manager = window.db_manager();
|
let query_manager = window.db_manager();
|
||||||
let file = read_import_file(file_path)?;
|
|
||||||
let plugin_context = window.plugin_context();
|
let plugin_context = window.plugin_context();
|
||||||
let workspace_context = WorkspaceContext {
|
let workspace_context = WorkspaceContext {
|
||||||
workspace_id: window.workspace_id(),
|
workspace_id: window.workspace_id(),
|
||||||
@@ -30,11 +46,57 @@ pub(crate) async fn import_data<R: Runtime>(
|
|||||||
plugin_manager: &plugin_manager,
|
plugin_manager: &plugin_manager,
|
||||||
plugin_context: &plugin_context,
|
plugin_context: &plugin_context,
|
||||||
workspace_context,
|
workspace_context,
|
||||||
contents: &file,
|
contents,
|
||||||
})
|
})
|
||||||
.await?)
|
.await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Download an importable document (OpenAPI, Postman, Insomnia, …) so it can be fed to the same
|
||||||
|
/// pipeline as a file on disk.
|
||||||
|
///
|
||||||
|
/// This uses Yaak's own API client, which follows the OS proxy but not the workspace's proxy,
|
||||||
|
/// client certificate, or certificate-validation settings. Requests are unauthenticated, so
|
||||||
|
/// specs behind auth must still be downloaded manually and imported as a file.
|
||||||
|
async fn fetch_import_url<R: Runtime>(window: &WebviewWindow<R>, url: &str) -> Result<String> {
|
||||||
|
let url = normalize_import_url(url)?;
|
||||||
|
let app_version = window.app_handle().package_info().version.to_string();
|
||||||
|
let response = yaak_api_client(ApiClientKind::App, &app_version)?
|
||||||
|
.get(&url)
|
||||||
|
// The API client defaults to JSON, but specs are just as often YAML
|
||||||
|
.header("Accept", "*/*")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|err| Error::GenericError(format!("Failed to fetch {url}: {err}")))?;
|
||||||
|
|
||||||
|
let status = response.status();
|
||||||
|
if !status.is_success() {
|
||||||
|
return Err(Error::GenericError(format!("Failed to fetch {url}: responded with {status}")));
|
||||||
|
}
|
||||||
|
|
||||||
|
response
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.map_err(|err| Error::GenericError(format!("Failed to read response from {url}: {err}")))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_import_url(url: &str) -> Result<String> {
|
||||||
|
let url = url.trim();
|
||||||
|
if url.is_empty() {
|
||||||
|
return Err(Error::GenericError("Import URL must not be empty".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if url.starts_with("http://") || url.starts_with("https://") {
|
||||||
|
return Ok(url.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
match url.split_once("://") {
|
||||||
|
Some((scheme, _)) => {
|
||||||
|
Err(Error::GenericError(format!("Import URL must be http or https, but got {scheme}")))
|
||||||
|
}
|
||||||
|
None => Ok(format!("https://{url}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn read_import_file(file_path: &str) -> Result<String> {
|
fn read_import_file(file_path: &str) -> Result<String> {
|
||||||
read_to_string(file_path).map_err(|err| {
|
read_to_string(file_path).map_err(|err| {
|
||||||
if err.kind() == ErrorKind::InvalidData {
|
if err.kind() == ErrorKind::InvalidData {
|
||||||
@@ -71,4 +133,22 @@ mod tests {
|
|||||||
|
|
||||||
remove_file(path).expect("remove binary fixture");
|
remove_file(path).expect("remove binary fixture");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalize_import_url_defaults_to_https() {
|
||||||
|
assert_eq!(
|
||||||
|
normalize_import_url(" example.com/openapi.yaml ").unwrap(),
|
||||||
|
"https://example.com/openapi.yaml"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
normalize_import_url("http://example.com/openapi.yaml").unwrap(),
|
||||||
|
"http://example.com/openapi.yaml"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalize_import_url_rejects_other_schemes() {
|
||||||
|
assert!(normalize_import_url("file:///tmp/openapi.yaml").is_err());
|
||||||
|
assert!(normalize_import_url(" ").is_err());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use crate::error::Error::GenericError;
|
|||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::grpc::{build_metadata, metadata_to_map, resolve_grpc_request};
|
use crate::grpc::{build_metadata, metadata_to_map, resolve_grpc_request};
|
||||||
use crate::http_request::{resolve_http_request, send_http_request};
|
use crate::http_request::{resolve_http_request, send_http_request};
|
||||||
use crate::import::import_data;
|
use crate::import::{import_data, import_url};
|
||||||
use crate::models_ext::{BlobManagerExt, QueryManagerExt};
|
use crate::models_ext::{BlobManagerExt, QueryManagerExt};
|
||||||
use crate::notifications::YaakNotifier;
|
use crate::notifications::YaakNotifier;
|
||||||
use crate::render::{render_grpc_request, render_json_value, render_template};
|
use crate::render::{render_grpc_request, render_json_value, render_template};
|
||||||
@@ -1165,6 +1165,13 @@ async fn cmd_import_data<R: Runtime>(
|
|||||||
import_data(&window, file_path).await
|
import_data(&window, file_path).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn cmd_import_url<R: Runtime>(
|
||||||
|
window: WebviewWindow<R>,
|
||||||
|
url: &str,
|
||||||
|
) -> YaakResult<BatchUpsertResult> {
|
||||||
|
import_url(&window, url).await
|
||||||
|
}
|
||||||
|
|
||||||
async fn cmd_http_request_actions<R: Runtime>(
|
async fn cmd_http_request_actions<R: Runtime>(
|
||||||
window: WebviewWindow<R>,
|
window: WebviewWindow<R>,
|
||||||
plugin_manager: State<'_, PluginManager>,
|
plugin_manager: State<'_, PluginManager>,
|
||||||
|
|||||||
@@ -320,6 +320,13 @@ pub(crate) struct CmdImportDataReq {
|
|||||||
pub file_path: String,
|
pub file_path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export, export_to = "gen_rpc.ts")]
|
||||||
|
pub(crate) struct CmdImportUrlReq {
|
||||||
|
pub url: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, TS)]
|
#[derive(Debug, Deserialize, TS)]
|
||||||
#[ts(export, export_to = "gen_rpc.ts")]
|
#[ts(export, export_to = "gen_rpc.ts")]
|
||||||
pub(crate) struct CmdHttpRequestActionsReq {}
|
pub(crate) struct CmdHttpRequestActionsReq {}
|
||||||
@@ -1026,6 +1033,10 @@ async fn cmd_import_data<R: Runtime>(ctx: ClientCtx<R>, req: CmdImportDataReq) -
|
|||||||
Ok(crate::cmd_import_data(ctx.window.clone(), &req.file_path).await?)
|
Ok(crate::cmd_import_data(ctx.window.clone(), &req.file_path).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn cmd_import_url<R: Runtime>(ctx: ClientCtx<R>, req: CmdImportUrlReq) -> Result<BatchUpsertResult> {
|
||||||
|
Ok(crate::cmd_import_url(ctx.window.clone(), &req.url).await?)
|
||||||
|
}
|
||||||
|
|
||||||
async fn cmd_http_request_actions<R: Runtime>(ctx: ClientCtx<R>, _req: CmdHttpRequestActionsReq) -> Result<Vec<GetHttpRequestActionsResponse>> {
|
async fn cmd_http_request_actions<R: Runtime>(ctx: ClientCtx<R>, _req: CmdHttpRequestActionsReq) -> Result<Vec<GetHttpRequestActionsResponse>> {
|
||||||
Ok(crate::cmd_http_request_actions(ctx.window.clone(), ctx.window.app_handle().state::<PluginManager>()).await?)
|
Ok(crate::cmd_http_request_actions(ctx.window.clone(), ctx.window.app_handle().state::<PluginManager>()).await?)
|
||||||
}
|
}
|
||||||
@@ -1400,6 +1411,7 @@ rpc_commands! {
|
|||||||
cmd_get_sse_events(CmdGetSseEventsReq) -> Vec<ServerSentEvent>,
|
cmd_get_sse_events(CmdGetSseEventsReq) -> Vec<ServerSentEvent>,
|
||||||
cmd_get_http_response_events(CmdGetHttpResponseEventsReq) -> Vec<HttpResponseEvent>,
|
cmd_get_http_response_events(CmdGetHttpResponseEventsReq) -> Vec<HttpResponseEvent>,
|
||||||
cmd_import_data(CmdImportDataReq) -> BatchUpsertResult,
|
cmd_import_data(CmdImportDataReq) -> BatchUpsertResult,
|
||||||
|
cmd_import_url(CmdImportUrlReq) -> BatchUpsertResult,
|
||||||
cmd_http_request_actions(CmdHttpRequestActionsReq) -> Vec<GetHttpRequestActionsResponse>,
|
cmd_http_request_actions(CmdHttpRequestActionsReq) -> Vec<GetHttpRequestActionsResponse>,
|
||||||
cmd_websocket_request_actions(CmdWebsocketRequestActionsReq) -> Vec<GetWebsocketRequestActionsResponse>,
|
cmd_websocket_request_actions(CmdWebsocketRequestActionsReq) -> Vec<GetWebsocketRequestActionsResponse>,
|
||||||
cmd_call_websocket_request_action(CmdCallWebsocketRequestActionReq) -> (),
|
cmd_call_websocket_request_action(CmdCallWebsocketRequestActionReq) -> (),
|
||||||
|
|||||||
Reference in New Issue
Block a user