mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-01-17 09:06:38 +01:00
Compare commits
12 Commits
v2023.0.16
...
v2023.0.19
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c456fd4d5 | ||
|
|
38c247e350 | ||
|
|
0c8f72124a | ||
|
|
80ed6b1525 | ||
|
|
4424b3f208 | ||
|
|
2c75abce09 | ||
|
|
4e15eb197f | ||
|
|
a7544b4f8c | ||
|
|
d126aad172 | ||
|
|
acc5c0de50 | ||
|
|
3391da111d | ||
|
|
e37ce96956 |
3
.github/workflows/artifacts.yml
vendored
3
.github/workflows/artifacts.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
include:
|
||||
- os: macos-12
|
||||
target: aarch64-apple-darwin
|
||||
- os: macos-12
|
||||
- os: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
- os: windows-2022
|
||||
target: x86_64-pc-windows-msvc
|
||||
@@ -58,6 +58,7 @@ jobs:
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
with:
|
||||
tagName: 'v__VERSION__'
|
||||
|
||||
972
package-lock.json
generated
972
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -33,7 +33,7 @@
|
||||
"@tanstack/react-query": "^4.28.0",
|
||||
"@tanstack/react-query-devtools": "^4.28.0",
|
||||
"@tanstack/react-query-persist-client": "^4.28.0",
|
||||
"@tauri-apps/api": "^1.2.0",
|
||||
"@tauri-apps/api": "^1.5.1",
|
||||
"@vitejs/plugin-react": "^3.1.0",
|
||||
"buffer": "^6.0.3",
|
||||
"classnames": "^2.3.2",
|
||||
@@ -55,7 +55,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/nesting": "^0.0.0-insiders.565cd3e",
|
||||
"@tauri-apps/cli": "^1.2.2",
|
||||
"@tauri-apps/cli": "^1.5.4",
|
||||
"@types/node": "^18.7.10",
|
||||
"@types/papaparse": "^5.3.7",
|
||||
"@types/parse-color": "^1.0.1",
|
||||
|
||||
919
src-tauri/Cargo.lock
generated
919
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -15,18 +15,18 @@ tauri-build = { version = "1.2", features = [] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
objc = "0.2.7"
|
||||
cocoa = "0.24.1"
|
||||
cocoa = "0.25.0"
|
||||
|
||||
[dependencies]
|
||||
serde_json = { version = "1.0", features = ["raw_value"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { version = "1.2", features = ["config-toml", "devtools", "fs-read-file", "os-all", "protocol-asset", "shell-open", "system-tray", "updater", "window-start-dragging"] }
|
||||
tauri = { version = "1.3", features = ["config-toml", "devtools", "fs-read-file", "os-all", "protocol-asset", "shell-open", "system-tray", "updater", "window-start-dragging"] }
|
||||
http = "0.2.8"
|
||||
reqwest = { version = "0.11.14", features = ["json"] }
|
||||
tokio = { version = "1.25.0", features = ["sync"] }
|
||||
futures = "0.3.26"
|
||||
deno_core = "0.179.0"
|
||||
deno_ast = { version = "0.25.0", features = ["transpiling"] }
|
||||
deno_core = "0.222.0"
|
||||
deno_ast = { version = "0.29.5", features = ["transpiling"] }
|
||||
sqlx = { version = "0.6.2", features = ["sqlite", "runtime-tokio-rustls", "json", "chrono", "time", "offline"] }
|
||||
uuid = "1.3.0"
|
||||
rand = "0.8.5"
|
||||
|
||||
@@ -233,7 +233,10 @@ async fn actually_send_ephemeral_request(
|
||||
let dir = app_handle.path_resolver().app_data_dir().unwrap();
|
||||
let base_dir = dir.join("responses");
|
||||
create_dir_all(base_dir.clone()).expect("Failed to create responses dir");
|
||||
let body_path = base_dir.join(response.id.clone());
|
||||
let body_path = match response.id == "" {
|
||||
false => base_dir.join(response.id.clone()),
|
||||
true => base_dir.join(uuid::Uuid::new_v4().to_string()),
|
||||
};
|
||||
let mut f = File::options()
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::pin::Pin;
|
||||
use std::rc::Rc;
|
||||
|
||||
use deno_ast::{MediaType, ParseParams, SourceTextInfo};
|
||||
use deno_core::anyhow::{anyhow, bail, Error};
|
||||
use deno_core::error::AnyError;
|
||||
use deno_core::futures::FutureExt;
|
||||
use deno_core::{op, Extension, JsRuntime, ModuleCode, ModuleSource, ModuleType, RuntimeOptions};
|
||||
use deno_core::{
|
||||
resolve_import, Extension, JsRuntime, ModuleLoader, ModuleSource, ModuleSourceFuture,
|
||||
ModuleSpecifier, ModuleType, ResolutionKind, RuntimeOptions, SourceMapGetter,
|
||||
};
|
||||
use futures::executor;
|
||||
|
||||
pub fn run_plugin_sync(file_path: &str) -> Result<(), AnyError> {
|
||||
@@ -11,19 +18,24 @@ pub fn run_plugin_sync(file_path: &str) -> Result<(), AnyError> {
|
||||
}
|
||||
|
||||
pub async fn run_plugin(file_path: &str) -> Result<(), AnyError> {
|
||||
let extension = Extension::builder("runtime")
|
||||
.ops(vec![op_hello::decl()])
|
||||
.build();
|
||||
let extension = Extension {
|
||||
name: "runtime",
|
||||
// ops: std::borrow::Cow::Borrowed(&[op_hello::DECL]),
|
||||
..Default::default()
|
||||
};
|
||||
let source_map_store = SourceMapStore(Rc::new(RefCell::new(HashMap::new())));
|
||||
|
||||
// Initialize a runtime instance
|
||||
let mut runtime = JsRuntime::new(RuntimeOptions {
|
||||
module_loader: Some(Rc::new(TsModuleLoader)),
|
||||
module_loader: Some(Rc::new(TypescriptModuleLoader {
|
||||
source_maps: source_map_store.clone(),
|
||||
})),
|
||||
extensions: vec![extension],
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
runtime
|
||||
.execute_script("<runtime>", include_str!("runtime.js"))
|
||||
.execute_script_static("<runtime>", include_str!("runtime.js"))
|
||||
.expect("Failed to execute runtime.js");
|
||||
|
||||
let current_dir = &std::env::current_dir().expect("Unable to get CWD");
|
||||
@@ -41,41 +53,50 @@ pub async fn run_plugin(file_path: &str) -> Result<(), AnyError> {
|
||||
result.await?
|
||||
}
|
||||
|
||||
#[op]
|
||||
async fn op_hello(name: String) -> Result<String, AnyError> {
|
||||
let contents = format!("Hello {} from Rust!", name);
|
||||
println!("{}", contents);
|
||||
Ok(contents)
|
||||
#[derive(Clone)]
|
||||
struct SourceMapStore(Rc<RefCell<HashMap<String, Vec<u8>>>>);
|
||||
|
||||
impl SourceMapGetter for SourceMapStore {
|
||||
fn get_source_map(&self, specifier: &str) -> Option<Vec<u8>> {
|
||||
self.0.borrow().get(specifier).cloned()
|
||||
}
|
||||
|
||||
fn get_source_line(&self, _file_name: &str, _line_number: usize) -> Option<String> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
struct TsModuleLoader;
|
||||
struct TypescriptModuleLoader {
|
||||
source_maps: SourceMapStore,
|
||||
}
|
||||
|
||||
impl deno_core::ModuleLoader for TsModuleLoader {
|
||||
impl ModuleLoader for TypescriptModuleLoader {
|
||||
fn resolve(
|
||||
&self,
|
||||
specifier: &str,
|
||||
referrer: &str,
|
||||
_kind: deno_core::ResolutionKind,
|
||||
) -> Result<deno_core::ModuleSpecifier, AnyError> {
|
||||
deno_core::resolve_import(specifier, referrer).map_err(|e| e.into())
|
||||
_kind: ResolutionKind,
|
||||
) -> Result<ModuleSpecifier, Error> {
|
||||
Ok(resolve_import(specifier, referrer)?)
|
||||
}
|
||||
|
||||
fn load(
|
||||
&self,
|
||||
module_specifier: &deno_core::ModuleSpecifier,
|
||||
_maybe_referrer: Option<deno_core::ModuleSpecifier>,
|
||||
module_specifier: &ModuleSpecifier,
|
||||
_maybe_referrer: Option<&ModuleSpecifier>,
|
||||
_is_dyn_import: bool,
|
||||
) -> std::pin::Pin<Box<deno_core::ModuleSourceFuture>> {
|
||||
let module_specifier = module_specifier.clone();
|
||||
async move {
|
||||
) -> Pin<Box<ModuleSourceFuture>> {
|
||||
let source_maps = self.source_maps.clone();
|
||||
fn load(
|
||||
source_maps: SourceMapStore,
|
||||
module_specifier: &ModuleSpecifier,
|
||||
) -> Result<ModuleSource, AnyError> {
|
||||
let path = module_specifier
|
||||
.to_file_path()
|
||||
.expect("Failed to convert to file path");
|
||||
.map_err(|_| anyhow!("Only file:// URLs are supported."))?;
|
||||
|
||||
// Determine what the MediaType is (this is done based on the file
|
||||
// extension) and whether transpiling is required.
|
||||
let media_type = MediaType::from_path(&path);
|
||||
let (module_type, should_transpile) = match media_type {
|
||||
let (module_type, should_transpile) = match MediaType::from_path(&path) {
|
||||
MediaType::JavaScript | MediaType::Mjs | MediaType::Cjs => {
|
||||
(ModuleType::JavaScript, false)
|
||||
}
|
||||
@@ -88,10 +109,9 @@ impl deno_core::ModuleLoader for TsModuleLoader {
|
||||
| MediaType::Dcts
|
||||
| MediaType::Tsx => (ModuleType::JavaScript, true),
|
||||
MediaType::Json => (ModuleType::Json, false),
|
||||
_ => panic!("Unknown extension {:?}", path.extension()),
|
||||
_ => bail!("Unknown extension {:?}", path.extension()),
|
||||
};
|
||||
|
||||
// Read the file, transpile if necessary.
|
||||
let code = std::fs::read_to_string(&path)?;
|
||||
let code = if should_transpile {
|
||||
let parsed = deno_ast::parse_module(ParseParams {
|
||||
@@ -102,20 +122,28 @@ impl deno_core::ModuleLoader for TsModuleLoader {
|
||||
scope_analysis: false,
|
||||
maybe_syntax: None,
|
||||
})?;
|
||||
parsed.transpile(&Default::default())?.text
|
||||
let res = parsed.transpile(&deno_ast::EmitOptions {
|
||||
inline_source_map: false,
|
||||
source_map: true,
|
||||
inline_sources: true,
|
||||
..Default::default()
|
||||
})?;
|
||||
let source_map = res.source_map.unwrap();
|
||||
source_maps
|
||||
.0
|
||||
.borrow_mut()
|
||||
.insert(module_specifier.to_string(), source_map.into_bytes());
|
||||
res.text
|
||||
} else {
|
||||
code
|
||||
};
|
||||
|
||||
// Load and return module.
|
||||
let module = ModuleSource {
|
||||
code: ModuleCode::from(code),
|
||||
Ok(ModuleSource::new(
|
||||
module_type,
|
||||
module_url_specified: module_specifier.to_string(),
|
||||
module_url_found: module_specifier.to_string(),
|
||||
};
|
||||
Ok(module)
|
||||
code.into(),
|
||||
module_specifier,
|
||||
))
|
||||
}
|
||||
.boxed_local()
|
||||
|
||||
futures::future::ready(load(source_maps, module_specifier)).boxed_local()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "Yaak",
|
||||
"version": "2023.0.16"
|
||||
"version": "2023.0.19"
|
||||
},
|
||||
"tauri": {
|
||||
"windows": [],
|
||||
@@ -49,13 +49,20 @@
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"identifier": "co.schier.yaak",
|
||||
"longDescription": "",
|
||||
"longDescription": "The best cross-platform visual API client",
|
||||
"resources": [
|
||||
"plugins/*",
|
||||
"migrations/*"
|
||||
],
|
||||
"shortDescription": "",
|
||||
"targets": "all",
|
||||
"shortDescription": "The best API client",
|
||||
"targets": [
|
||||
"deb",
|
||||
"appimage",
|
||||
"nsis",
|
||||
"app",
|
||||
"dmg",
|
||||
"updater"
|
||||
],
|
||||
"deb": {
|
||||
"depends": []
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { HttpRequest } from '../lib/models';
|
||||
import { Button } from './core/Button';
|
||||
import type { EditorProps } from './core/Editor';
|
||||
import { Editor, formatGraphQL } from './core/Editor';
|
||||
import { FormattedError } from './core/FormattedError';
|
||||
import { Separator } from './core/Separator';
|
||||
import { useDialog } from './DialogContext';
|
||||
|
||||
@@ -24,8 +25,7 @@ interface GraphQLBody {
|
||||
|
||||
export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEditorProps }: Props) {
|
||||
const editorViewRef = useRef<EditorView>(null);
|
||||
const introspection = useIntrospectGraphQL(baseRequest);
|
||||
|
||||
const { schema, isLoading, error, refetch } = useIntrospectGraphQL(baseRequest);
|
||||
const { query, variables } = useMemo<GraphQLBody>(() => {
|
||||
if (defaultValue === undefined) {
|
||||
return { query: '', variables: {} };
|
||||
@@ -65,8 +65,8 @@ export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEdi
|
||||
// Refetch the schema when the URL changes
|
||||
useEffect(() => {
|
||||
if (editorViewRef.current === null) return;
|
||||
updateSchema(editorViewRef.current, introspection.data);
|
||||
}, [introspection.data]);
|
||||
updateSchema(editorViewRef.current, schema);
|
||||
}, [schema]);
|
||||
|
||||
const dialog = useDialog();
|
||||
|
||||
@@ -81,22 +81,38 @@ export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEdi
|
||||
placeholder="..."
|
||||
ref={editorViewRef}
|
||||
actions={
|
||||
(introspection.error || introspection.isLoading) && (
|
||||
(error || isLoading) && (
|
||||
<Button
|
||||
size="xs"
|
||||
color={introspection.error ? 'danger' : 'gray'}
|
||||
isLoading={introspection.isLoading}
|
||||
color={error ? 'danger' : 'gray'}
|
||||
isLoading={isLoading}
|
||||
onClick={() => {
|
||||
dialog.show({
|
||||
title: 'Introspection Failed',
|
||||
size: 'sm',
|
||||
id: 'introspection-failed',
|
||||
render: () => (
|
||||
<div className="whitespace-pre-wrap">{introspection.error?.message}</div>
|
||||
<div className="whitespace-pre-wrap">
|
||||
<FormattedError>{error ?? 'unknown'}</FormattedError>
|
||||
<div className="w-full mt-3">
|
||||
<Button
|
||||
onClick={() => {
|
||||
dialog.hide('introspection-failed');
|
||||
refetch();
|
||||
}}
|
||||
className="ml-auto"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
>
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{introspection.error ? 'Introspection Failed' : 'Introspecting'}
|
||||
{error ? 'Introspection Failed' : 'Introspecting'}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useRouteError } from 'react-router-dom';
|
||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { Button } from './core/Button';
|
||||
import { FormattedError } from './core/FormattedError';
|
||||
import { Heading } from './core/Heading';
|
||||
import { VStack } from './core/Stacks';
|
||||
|
||||
@@ -14,9 +15,7 @@ export default function RouteError() {
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<VStack space={5} className="max-w-[30rem] !h-auto">
|
||||
<Heading>Route Error 🔥</Heading>
|
||||
<pre className="text-sm select-auto cursor-text bg-gray-100 p-3 rounded whitespace-normal">
|
||||
{message}
|
||||
</pre>
|
||||
<FormattedError>{message}</FormattedError>
|
||||
<VStack space={2}>
|
||||
<Button
|
||||
color="primary"
|
||||
|
||||
@@ -242,6 +242,7 @@ type SidebarItemProps = {
|
||||
useProminentStyles?: boolean;
|
||||
selected?: boolean;
|
||||
onSelect: (requestId: string) => void;
|
||||
draggable?: boolean;
|
||||
};
|
||||
|
||||
const _SidebarItem = forwardRef(function SidebarItem(
|
||||
@@ -300,16 +301,13 @@ const _SidebarItem = forwardRef(function SidebarItem(
|
||||
return (
|
||||
<li ref={ref} className={classnames(className, 'block group/item px-2 pb-0.5')}>
|
||||
<button
|
||||
tabIndex={-1}
|
||||
color="custom"
|
||||
// tabIndex={-1} // Will prevent drag-n-drop
|
||||
onClick={handleSelect}
|
||||
disabled={editing}
|
||||
draggable={false} // Item should drag, not the link
|
||||
onDoubleClick={handleStartEditing}
|
||||
data-active={isActive}
|
||||
data-selected={selected}
|
||||
className={classnames(
|
||||
// 'outline-none',
|
||||
'w-full flex items-center text-sm h-xs px-2 rounded-md transition-colors',
|
||||
editing && 'ring-1 focus-within:ring-focus',
|
||||
isActive && 'bg-highlight text-gray-800',
|
||||
@@ -396,6 +394,7 @@ const DraggableSidebarItem = memo(function DraggableSidebarItem({
|
||||
return (
|
||||
<SidebarItem
|
||||
ref={ref}
|
||||
draggable
|
||||
className={classnames(isDragging && 'opacity-20')}
|
||||
requestName={requestName}
|
||||
requestId={requestId}
|
||||
|
||||
@@ -156,10 +156,10 @@
|
||||
|
||||
/* NOTE: Extra selector required to override default styles */
|
||||
.cm-tooltip.cm-tooltip {
|
||||
@apply shadow-lg bg-gray-50 rounded text-gray-700 border border-gray-200 z-50 pointer-events-auto text-sm;
|
||||
@apply shadow-lg bg-gray-50 rounded text-gray-700 border border-gray-200 z-50 pointer-events-auto text-[0.75rem];
|
||||
|
||||
&.cm-completionInfo-right {
|
||||
@apply ml-1;
|
||||
@apply ml-1 -mt-0.5 text-sm;
|
||||
}
|
||||
|
||||
&.cm-completionInfo-right-narrow {
|
||||
|
||||
11
src-web/components/core/FormattedError.tsx
Normal file
11
src-web/components/core/FormattedError.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
interface Props {
|
||||
children: string;
|
||||
}
|
||||
|
||||
export function FormattedError({ children }: Props) {
|
||||
return (
|
||||
<pre className="text-sm select-auto cursor-text bg-gray-100 p-3 rounded whitespace-normal border border-red-500 border-dashed">
|
||||
{children}
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { GraphQLSchema } from 'graphql';
|
||||
import type { IntrospectionQuery } from 'graphql';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
import { buildClientSchema, getIntrospectionQuery } from '../components/core/Editor';
|
||||
import { minPromiseMillis } from '../lib/minPromiseMillis';
|
||||
import type { HttpRequest } from '../lib/models';
|
||||
import { getResponseBodyText } from '../lib/responseBody';
|
||||
import { sendEphemeralRequest } from '../lib/sendEphemeralRequest';
|
||||
import { useDebouncedValue } from './useDebouncedValue';
|
||||
|
||||
@@ -12,18 +14,24 @@ const introspectionRequestBody = JSON.stringify({
|
||||
});
|
||||
|
||||
export function useIntrospectGraphQL(baseRequest: HttpRequest) {
|
||||
// Debounce the URL because it can change rapidly, and we don't
|
||||
// want to send so many requests.
|
||||
// Debounce the request because it can change rapidly and we don't
|
||||
// want to send so too many requests.
|
||||
const request = useDebouncedValue(baseRequest);
|
||||
const [refetchKey, setRefetchKey] = useState<number>(0);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string>();
|
||||
const [introspection, setIntrospection] = useLocalStorage<IntrospectionQuery>(
|
||||
`introspection:${baseRequest.id}`,
|
||||
);
|
||||
|
||||
return useQuery<GraphQLSchema, Error>({
|
||||
queryKey: ['introspectGraphQL', { url: request.url, method: request.method }],
|
||||
refetchInterval: 1000 * 60, // Refetch every minute
|
||||
queryFn: async () => {
|
||||
const response = await minPromiseMillis(
|
||||
sendEphemeralRequest({ ...baseRequest, body: introspectionRequestBody }),
|
||||
700,
|
||||
);
|
||||
const introspectionInterval = useRef<NodeJS.Timeout>();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchIntrospection = async () => {
|
||||
setIsLoading(true);
|
||||
setError(undefined);
|
||||
const args = { ...baseRequest, body: introspectionRequestBody };
|
||||
const response = await minPromiseMillis(sendEphemeralRequest(args), 700);
|
||||
|
||||
if (response.error) {
|
||||
return Promise.reject(new Error(response.error));
|
||||
@@ -35,12 +43,37 @@ export function useIntrospectGraphQL(baseRequest: HttpRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
if (response.body === null) {
|
||||
const body = await getResponseBodyText(response);
|
||||
if (body === null) {
|
||||
return Promise.reject(new Error('Empty body returned in response'));
|
||||
}
|
||||
|
||||
const { data } = JSON.parse(response.body);
|
||||
return buildClientSchema(data);
|
||||
},
|
||||
});
|
||||
const { data } = JSON.parse(body);
|
||||
setIntrospection(data);
|
||||
};
|
||||
|
||||
const runIntrospection = () => {
|
||||
fetchIntrospection()
|
||||
.catch((e) => setError(e.message))
|
||||
.finally(() => setIsLoading(false));
|
||||
};
|
||||
|
||||
// Do it again on an interval
|
||||
clearInterval(introspectionInterval.current);
|
||||
introspectionInterval.current = setInterval(runIntrospection, 1000 * 60);
|
||||
runIntrospection(); // Run immediately
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [request.id, request.url, request.method, refetchKey]);
|
||||
|
||||
const refetch = useCallback(() => {
|
||||
setRefetchKey((k) => k + 1);
|
||||
}, []);
|
||||
|
||||
const schema = useMemo(
|
||||
() => (introspection ? buildClientSchema(introspection) : undefined),
|
||||
[introspection],
|
||||
);
|
||||
|
||||
return { schema, isLoading, error, refetch };
|
||||
}
|
||||
|
||||
@@ -1,20 +1,12 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { readBinaryFile } from '@tauri-apps/api/fs';
|
||||
import type { HttpResponse } from '../lib/models';
|
||||
import { getResponseBodyBlob } from '../lib/responseBody';
|
||||
|
||||
export function useResponseBodyBlob(response: HttpResponse) {
|
||||
return useQuery<Uint8Array | null>({
|
||||
enabled: response != null,
|
||||
queryKey: ['response-body-binary', response?.updatedAt],
|
||||
initialData: null,
|
||||
queryFn: async () => {
|
||||
if (response.body) {
|
||||
return Uint8Array.of(...response.body);
|
||||
}
|
||||
if (response.bodyPath) {
|
||||
return readBinaryFile(response.bodyPath);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
queryFn: () => getResponseBodyBlob(response),
|
||||
}).data;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,11 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { readTextFile } from '@tauri-apps/api/fs';
|
||||
import type { HttpResponse } from '../lib/models';
|
||||
import { getResponseBodyText } from '../lib/responseBody';
|
||||
|
||||
export function useResponseBodyText(response: HttpResponse) {
|
||||
return useQuery<string | null>({
|
||||
queryKey: ['response-body-text', response?.updatedAt],
|
||||
initialData: null,
|
||||
queryFn: async () => {
|
||||
if (response.body) {
|
||||
const uint8Array = Uint8Array.of(...response.body);
|
||||
return new TextDecoder().decode(uint8Array);
|
||||
}
|
||||
|
||||
if (response.bodyPath) {
|
||||
return await readTextFile(response.bodyPath);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
queryFn: () => getResponseBodyText(response),
|
||||
}).data;
|
||||
}
|
||||
|
||||
23
src-web/lib/responseBody.ts
Normal file
23
src-web/lib/responseBody.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { readBinaryFile, readTextFile } from '@tauri-apps/api/fs';
|
||||
import type { HttpResponse } from './models';
|
||||
|
||||
export async function getResponseBodyText(response: HttpResponse): Promise<string | null> {
|
||||
if (response.body) {
|
||||
const uint8Array = Uint8Array.from(response.body);
|
||||
return new TextDecoder().decode(uint8Array);
|
||||
}
|
||||
if (response.bodyPath) {
|
||||
return await readTextFile(response.bodyPath);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getResponseBodyBlob(response: HttpResponse): Promise<Uint8Array | null> {
|
||||
if (response.body) {
|
||||
return Uint8Array.from(response.body);
|
||||
}
|
||||
if (response.bodyPath) {
|
||||
return readBinaryFile(response.bodyPath);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import type { HttpRequest, HttpResponse } from './models';
|
||||
|
||||
export function sendEphemeralRequest(request: HttpRequest): Promise<HttpResponse> {
|
||||
export async function sendEphemeralRequest(request: HttpRequest): Promise<HttpResponse> {
|
||||
// Remove some things that we don't want to associate
|
||||
const newRequest = { ...request, id: '', requestId: '', workspaceId: '' };
|
||||
return invoke('send_ephemeral_request', { request: newRequest });
|
||||
|
||||
@@ -29,7 +29,21 @@ module.exports = {
|
||||
},
|
||||
fontFamily: {
|
||||
"mono": ["JetBrains Mono", "Menlo", "monospace"],
|
||||
"sans": ["Inter", "sans-serif"]
|
||||
"sans": [
|
||||
"Inter",
|
||||
"-apple-system",
|
||||
"BlinkMacSystemFont",
|
||||
"Segoe UI",
|
||||
"Roboto",
|
||||
"Oxygen-Sans",
|
||||
"Ubuntu",
|
||||
"Cantarell",
|
||||
"Helvetica Neue",
|
||||
"sans-serif",
|
||||
"Apple Color Emoji",
|
||||
"Segoe UI Emoji",
|
||||
"Segoe UI Symbol",
|
||||
],
|
||||
},
|
||||
fontSize: {
|
||||
'3xs': "0.6rem",
|
||||
|
||||
Reference in New Issue
Block a user