Compare commits

..

11 Commits

Author SHA1 Message Date
Gregory Schier
bb014b7c43 Remove folder/environment foreign keys to make sync/import easier, and simplify batch upsert code. 2025-04-24 19:57:02 -07:00
Gregory Schier
9fa0650647 Add scrollbar to sidebar
Fixes: https://feedback.yaak.app/p/missing-scrollbar-on-request-list
2025-04-22 07:48:34 -07:00
Gregory Schier
b8c42677ca Fix cmd+p filtering reference
https://feedback.yaak.app/p/search-doesnt-actually-search-through-all-the-apis
2025-04-22 07:46:10 -07:00
Gregory Schier
2eb3c2241c Fix duration tag
Closes: https://feedback.yaak.app/p/elapsed-time-not-stopping-on-failed-request
2025-04-22 07:29:17 -07:00
Gregory Schier
8fb7bbfe2e Don't prompt user for keychain password more than once 2025-04-22 07:23:05 -07:00
Gregory Schier
52eba74151 Handle no text 2025-04-22 07:01:48 -07:00
Gregory Schier
e651760713 Merge remote-tracking branch 'origin/master' 2025-04-22 06:59:11 -07:00
Gregory Schier
82451a26f6 Use mimeType for response viewer 2025-04-22 06:58:53 -07:00
jzhangdev
cc15f60fb6 Fix header layout (#182) 2025-04-22 06:51:39 -07:00
Gregory Schier
2f8b2a81c7 Fix jotai/index imports 2025-04-21 07:08:13 -07:00
Gregory Schier
6d4fdc91fe Fix text decoding when no content-type
Closes https://feedback.yaak.app/p/not-rendering-response
2025-04-21 06:54:03 -07:00
43 changed files with 343 additions and 136 deletions

View File

@@ -0,0 +1,245 @@
-- NOTE: SQLite does not support dropping foreign keys, so we need to create new
-- tables and copy data instead. To prevent cascade deletes from wrecking stuff,
-- we start with the leaf tables and finish with the parent tables (eg. folder).
----------------------------
-- Remove http request FK --
----------------------------
CREATE TABLE http_requests_dg_tmp
(
id TEXT NOT NULL
PRIMARY KEY,
model TEXT DEFAULT 'http_request' NOT NULL,
workspace_id TEXT NOT NULL
REFERENCES workspaces
ON DELETE CASCADE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
deleted_at DATETIME,
name TEXT NOT NULL,
url TEXT NOT NULL,
method TEXT NOT NULL,
headers TEXT NOT NULL,
body_type TEXT,
sort_priority REAL DEFAULT 0 NOT NULL,
authentication TEXT DEFAULT '{}' NOT NULL,
authentication_type TEXT,
folder_id TEXT,
body TEXT DEFAULT '{}' NOT NULL,
url_parameters TEXT DEFAULT '[]' NOT NULL,
description TEXT DEFAULT '' NOT NULL
);
INSERT INTO http_requests_dg_tmp(id, model, workspace_id, created_at, updated_at, deleted_at, name, url, method,
headers, body_type, sort_priority, authentication, authentication_type, folder_id,
body, url_parameters, description)
SELECT id,
model,
workspace_id,
created_at,
updated_at,
deleted_at,
name,
url,
method,
headers,
body_type,
sort_priority,
authentication,
authentication_type,
folder_id,
body,
url_parameters,
description
FROM http_requests;
DROP TABLE http_requests;
ALTER TABLE http_requests_dg_tmp
RENAME TO http_requests;
----------------------------
-- Remove grpc request FK --
----------------------------
CREATE TABLE grpc_requests_dg_tmp
(
id TEXT NOT NULL
PRIMARY KEY,
model TEXT DEFAULT 'grpc_request' NOT NULL,
workspace_id TEXT NOT NULL
REFERENCES workspaces
ON DELETE CASCADE,
folder_id TEXT,
created_at DATETIME DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')) NOT NULL,
updated_at DATETIME DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')) NOT NULL,
name TEXT NOT NULL,
sort_priority REAL NOT NULL,
url TEXT NOT NULL,
service TEXT,
method TEXT,
message TEXT NOT NULL,
authentication TEXT DEFAULT '{}' NOT NULL,
authentication_type TEXT,
metadata TEXT DEFAULT '[]' NOT NULL,
description TEXT DEFAULT '' NOT NULL
);
INSERT INTO grpc_requests_dg_tmp(id, model, workspace_id, folder_id, created_at, updated_at, name, sort_priority, url,
service, method, message, authentication, authentication_type, metadata, description)
SELECT id,
model,
workspace_id,
folder_id,
created_at,
updated_at,
name,
sort_priority,
url,
service,
method,
message,
authentication,
authentication_type,
metadata,
description
FROM grpc_requests;
DROP TABLE grpc_requests;
ALTER TABLE grpc_requests_dg_tmp
RENAME TO grpc_requests;
---------------------------------
-- Remove websocket request FK --
---------------------------------
CREATE TABLE websocket_requests_dg_tmp
(
id TEXT NOT NULL
PRIMARY KEY,
model TEXT DEFAULT 'websocket_request' NOT NULL,
workspace_id TEXT NOT NULL
REFERENCES workspaces
ON DELETE CASCADE,
folder_id TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
deleted_at DATETIME,
authentication TEXT DEFAULT '{}' NOT NULL,
authentication_type TEXT,
description TEXT NOT NULL,
name TEXT NOT NULL,
url TEXT NOT NULL,
headers TEXT NOT NULL,
message TEXT NOT NULL,
sort_priority REAL NOT NULL,
url_parameters TEXT DEFAULT '[]' NOT NULL
);
INSERT INTO websocket_requests_dg_tmp(id, model, workspace_id, folder_id, created_at, updated_at, deleted_at,
authentication, authentication_type, description, name, url, headers, message,
sort_priority, url_parameters)
SELECT id,
model,
workspace_id,
folder_id,
created_at,
updated_at,
deleted_at,
authentication,
authentication_type,
description,
name,
url,
headers,
message,
sort_priority,
url_parameters
FROM websocket_requests;
DROP TABLE websocket_requests;
ALTER TABLE websocket_requests_dg_tmp
RENAME TO websocket_requests;
PRAGMA foreign_keys = ON;
---------------------------
-- Remove environment FK --
---------------------------
CREATE TABLE environments_dg_tmp
(
id TEXT NOT NULL
PRIMARY KEY,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
deleted_at DATETIME,
workspace_id TEXT NOT NULL
REFERENCES workspaces
ON DELETE CASCADE,
name TEXT NOT NULL,
variables DEFAULT '[]' NOT NULL,
model TEXT DEFAULT 'environment',
environment_id TEXT
);
INSERT INTO environments_dg_tmp(id, created_at, updated_at, deleted_at, workspace_id, name, variables, model,
environment_id)
SELECT id,
created_at,
updated_at,
deleted_at,
workspace_id,
name,
variables,
model,
environment_id
FROM environments;
DROP TABLE environments;
ALTER TABLE environments_dg_tmp
RENAME TO environments;
----------------------
-- Remove folder FK --
----------------------
CREATE TABLE folders_dg_tmp
(
id TEXT NOT NULL
PRIMARY KEY,
model TEXT DEFAULT 'folder' NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
deleted_at DATETIME,
workspace_id TEXT NOT NULL
REFERENCES workspaces
ON DELETE CASCADE,
folder_id TEXT,
name TEXT NOT NULL,
sort_priority REAL DEFAULT 0 NOT NULL,
description TEXT DEFAULT '' NOT NULL
);
INSERT INTO folders_dg_tmp(id, model, created_at, updated_at, deleted_at, workspace_id, folder_id, name, sort_priority,
description)
SELECT id,
model,
created_at,
updated_at,
deleted_at,
workspace_id,
folder_id,
name,
sort_priority,
description
FROM folders;
DROP TABLE folders;
ALTER TABLE folders_dg_tmp
RENAME TO folders;

View File

@@ -9,7 +9,7 @@ use crate::updates::{UpdateMode, UpdateTrigger, YaakUpdater};
use crate::uri_scheme::handle_uri_scheme;
use error::Result as YaakResult;
use eventsource_client::{EventParser, SSE};
use log::{debug, error, warn};
use log::{debug, error, info, warn};
use std::collections::{BTreeMap, HashMap};
use std::fs::{File, create_dir_all};
use std::path::PathBuf;
@@ -880,6 +880,8 @@ async fn cmd_import_data<R: Runtime>(
})
.collect();
info!("Importing data");
let upserted = app_handle.with_tx(|tx| {
tx.batch_upsert(
workspaces,

View File

@@ -158,15 +158,14 @@ impl EncryptionManager {
}
fn get_master_key(&self) -> Result<MasterKey> {
{
let master_secret = self.cached_master_key.lock().unwrap();
if let Some(k) = master_secret.as_ref() {
return Ok(k.to_owned());
}
// NOTE: This locks the key for the entire function which seems wrong, but this prevents
// concurrent access from prompting the user for a keychain password multiple times.
let mut master_secret = self.cached_master_key.lock().unwrap();
if let Some(k) = master_secret.as_ref() {
return Ok(k.to_owned());
}
let mkey = MasterKey::get_or_create(&self.app_id, KEY_USER)?;
let mut master_secret = self.cached_master_key.lock().unwrap();
*master_secret = Some(mkey.clone());
Ok(mkey)
}

View File

@@ -1,4 +1,4 @@
import { createStore } from 'jotai/index';
import { createStore } from 'jotai';
import { AnyModel } from '../bindings/gen_models';
export type ExtractModel<T, M> = T extends { model: M } ? T : never;

View File

@@ -1,8 +1,8 @@
use crate::db_context::DbContext;
use crate::error::Result;
use crate::models::{Environment, Folder, GrpcRequest, HttpRequest, WebsocketRequest, Workspace};
use crate::util::{BatchUpsertResult, UpdateSource};
use log::info;
use crate::db_context::DbContext;
impl<'a> DbContext<'a> {
pub fn batch_upsert(
@@ -18,64 +18,19 @@ impl<'a> DbContext<'a> {
let mut imported_resources = BatchUpsertResult::default();
if workspaces.len() > 0 {
info!("Batch inserting {} workspaces", workspaces.len());
for v in workspaces {
let x = self.upsert_workspace(&v, source)?;
imported_resources.workspaces.push(x.clone());
}
}
if environments.len() > 0 {
while imported_resources.environments.len() < environments.len() {
for v in environments.clone() {
if let Some(id) = v.environment_id.clone() {
let has_parent_to_import =
environments.iter().find(|m| m.id == id).is_some();
let imported_parent =
imported_resources.environments.iter().find(|m| m.id == id);
// If there's also a parent to upsert, wait for that one
if imported_parent.is_none() && has_parent_to_import {
continue;
}
}
if let Some(_) = imported_resources.environments.iter().find(|f| f.id == v.id) {
continue;
}
let x = self.upsert_environment(&v, source)?;
imported_resources.environments.push(x.clone());
}
}
info!("Imported {} environments", imported_resources.environments.len());
}
if folders.len() > 0 {
while imported_resources.folders.len() < folders.len() {
for v in folders.clone() {
if let Some(id) = v.folder_id.clone() {
let has_parent_to_import = folders.iter().find(|m| m.id == id).is_some();
let imported_parent =
imported_resources.folders.iter().find(|m| m.id == id);
// If there's also a parent to upsert, wait for that one
if imported_parent.is_none() && has_parent_to_import {
continue;
}
}
if let Some(_) = imported_resources.folders.iter().find(|f| f.id == v.id) {
continue;
}
let x = self.upsert_folder(&v, source)?;
imported_resources.folders.push(x.clone());
}
}
info!("Imported {} folders", imported_resources.folders.len());
info!("Upserted {} workspaces", imported_resources.environments.len());
}
if http_requests.len() > 0 {
for v in http_requests {
let x = self.upsert(&v, source)?;
let x = self.upsert_http_request(&v, source)?;
imported_resources.http_requests.push(x.clone());
}
info!("Imported {} http_requests", imported_resources.http_requests.len());
info!("Upserted Imported {} http_requests", imported_resources.http_requests.len());
}
if grpc_requests.len() > 0 {
@@ -83,7 +38,7 @@ impl<'a> DbContext<'a> {
let x = self.upsert_grpc_request(&v, source)?;
imported_resources.grpc_requests.push(x.clone());
}
info!("Imported {} grpc_requests", imported_resources.grpc_requests.len());
info!("Upserted {} grpc_requests", imported_resources.grpc_requests.len());
}
if websocket_requests.len() > 0 {
@@ -91,7 +46,25 @@ impl<'a> DbContext<'a> {
let x = self.upsert_websocket_request(&v, source)?;
imported_resources.websocket_requests.push(x.clone());
}
info!("Imported {} websocket_requests", imported_resources.websocket_requests.len());
info!("Upserted {} websocket_requests", imported_resources.websocket_requests.len());
}
if environments.len() > 0 {
for x in environments {
let x = self.upsert_environment(&x, source)?;
imported_resources.environments.push(x.clone());
}
info!("Upserted {} environments", imported_resources.environments.len());
}
// Do folders last so it doesn't cause the UI to render empty folders before populating
// immediately after.
if folders.len() > 0 {
for v in folders {
let x = self.upsert_folder(&v, source)?;
imported_resources.folders.push(x.clone());
}
info!("Upserted {} folders", imported_resources.folders.len());
}
Ok(imported_resources)

View File

@@ -448,7 +448,7 @@ pub(crate) async fn apply_sync_ops<R: Runtime>(
websocket_requests_to_upsert,
&UpdateSource::Sync,
)?;
// Ensure we create WorkspaceMeta models for each new workspace, with the appropriate sync dir
let sync_dir_string = sync_dir.to_string_lossy().to_string();
for workspace in upserted_models.workspaces {

View File

@@ -11,6 +11,7 @@ export const openWorkspaceFromSyncDir = createFastMutation<void, void, string>({
const workspace = ops
.map((o) => (o.type === 'dbCreate' && o.fs.model.type === 'workspace' ? o.fs.model : null))
.filter((m) => m)[0];
if (workspace == null) {
showSimpleAlert('Failed to Open', 'No workspace found in directory');
return;

View File

@@ -1,7 +1,7 @@
import { workspacesAtom } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { fuzzyFilter } from 'fuzzbunny';
import { useAtomValue } from 'jotai/index';
import { useAtomValue } from 'jotai';
import type { KeyboardEvent, ReactNode } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createFolder } from '../commands/commands';
@@ -350,10 +350,10 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
const filteredGroups = groups
.map((g) => {
g.items = result
const items = result
.filter((i) => g.items.find((i2) => i2.key === i.key))
.slice(0, MAX_PER_GROUP);
return g;
return { ...g, items };
})
.filter((g) => g.items.length > 0);

View File

@@ -11,7 +11,7 @@ import type {
JsonPrimitive,
} from '@yaakapp-internal/plugins';
import classNames from 'classnames';
import { useAtomValue } from 'jotai/index';
import { useAtomValue } from 'jotai';
import { useCallback } from 'react';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { capitalize } from '../lib/capitalize';

View File

@@ -1,5 +1,5 @@
import { foldersAtom, patchModel } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai/index';
import { useAtomValue } from 'jotai';
import { Input } from './core/Input';
import { VStack } from './core/Stacks';
import { MarkdownEditor } from './MarkdownEditor';

View File

@@ -1,8 +1,7 @@
import type { GrpcEvent, GrpcRequest } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { format } from 'date-fns';
import { useAtomValue } from 'jotai';
import { useSetAtom } from 'jotai/index';
import { useAtomValue , useSetAtom } from 'jotai';
import type { CSSProperties } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useCopy } from '../hooks/useCopy';

View File

@@ -34,8 +34,8 @@ export function HeaderSize({
// Add padding for macOS stoplights, but keep it the same width (account for the interface scale)
paddingLeft:
stoplightsVisible && !ignoreControlsSpacing ? 72 / settings.interfaceScale : undefined,
...(size === 'md' ? { height: HEADER_SIZE_MD } : {}),
...(size === 'lg' ? { height: HEADER_SIZE_LG } : {}),
...(size === 'md' ? { minHeight: HEADER_SIZE_MD } : {}),
...(size === 'lg' ? { minHeight: HEADER_SIZE_LG } : {}),
...(osInfo.osType === 'macos' || ignoreControlsSpacing
? { paddingRight: '2px' }
: { paddingLeft: '2px', paddingRight: WINDOW_CONTROLS_WIDTH }),

View File

@@ -5,6 +5,7 @@ import React, { useCallback, useMemo } from 'react';
import { useLocalStorage } from 'react-use';
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
import { useResponseViewMode } from '../hooks/useResponseViewMode';
import { getMimeTypeFromContentType } from '../lib/contentType';
import { getContentTypeFromHeaders } from '../lib/model_util';
import { ConfirmLargeResponse } from './ConfirmLargeResponse';
import { Banner } from './core/Banner';
@@ -48,6 +49,7 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
{},
);
const contentType = getContentTypeFromHeaders(activeResponse?.headers ?? null);
const mimeType = contentType == null ? null : getMimeTypeFromContentType(contentType).essence;
const tabs = useMemo<TabItem[]>(
() => [
@@ -59,7 +61,7 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
onChange: setViewMode,
items: [
{ label: 'Pretty', value: 'pretty' },
...(contentType?.startsWith('image') ? [] : [{ label: 'Raw', value: 'raw' }]),
...(mimeType?.startsWith('image') ? [] : [{ label: 'Raw', value: 'raw' }]),
],
},
},
@@ -77,7 +79,7 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
label: 'Info',
},
],
[activeResponse?.headers, contentType, setViewMode, viewMode],
[activeResponse?.headers, mimeType, setViewMode, viewMode],
);
const activeTab = activeTabs?.[activeRequestId];
const setActiveTab = useCallback(
@@ -123,9 +125,7 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
{activeResponse.state !== 'closed' && <LoadingIcon size="sm" />}
<HttpStatusTag showReason response={activeResponse} />
<span>&bull;</span>
<HttpResponseDurationTag
response={activeResponse}
/>
<HttpResponseDurationTag response={activeResponse} />
<span>&bull;</span>
<SizeTag contentLength={activeResponse.contentLength ?? 0} />
@@ -162,23 +162,21 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
</EmptyStateText>
) : activeResponse.state === 'closed' && activeResponse.contentLength === 0 ? (
<EmptyStateText>Empty </EmptyStateText>
) : contentType?.match(/^text\/event-stream$/i) && viewMode === 'pretty' ? (
) : mimeType?.match(/^text\/event-stream/i) && viewMode === 'pretty' ? (
<EventStreamViewer response={activeResponse} />
) : contentType?.match(/^image\/svg/) ? (
) : mimeType?.match(/^image\/svg/) ? (
<SvgViewer response={activeResponse} />
) : contentType?.match(/^image/i) ? (
) : mimeType?.match(/^image/i) ? (
<EnsureCompleteResponse response={activeResponse} render={ImageViewer} />
) : contentType?.match(/^audio/i) ? (
) : mimeType?.match(/^audio/i) ? (
<EnsureCompleteResponse response={activeResponse} render={AudioViewer} />
) : contentType?.match(/^video/i) ? (
) : mimeType?.match(/^video/i) ? (
<EnsureCompleteResponse response={activeResponse} render={VideoViewer} />
) : contentType?.match(/pdf/i) ? (
) : mimeType?.match(/pdf/i) ? (
<EnsureCompleteResponse response={activeResponse} render={PdfViewer} />
) : contentType?.match(/csv|tab-separated/i) ? (
) : mimeType?.match(/csv|tab-separated/i) ? (
<CsvViewer className="pb-2" response={activeResponse} />
) : (
// ) : viewMode === 'pretty' && contentType?.includes('json') ? (
// <JsonAttributeTree attrValue={activeResponse} />
<HTMLOrTextViewer
textViewerClassName="-mr-2 bg-surface" // Pull to the right
response={activeResponse}

View File

@@ -1,6 +1,6 @@
import { revealItemInDir } from '@tauri-apps/plugin-opener';
import { patchModel, settingsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai/index';
import { useAtomValue } from 'jotai';
import React from 'react';
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
import { useAppInfo } from '../../hooks/useAppInfo';

View File

@@ -18,10 +18,10 @@ export function SyncToFilesystemSetting({
onCreateNewWorkspace,
value,
}: SyncToFilesystemSettingProps) {
const [isNonEmpty, setIsNonEmpty] = useState<string | null>(null);
const [syncDir, setSyncDir] = useState<string | null>(null);
return (
<VStack className="w-full my-2" space={3}>
{isNonEmpty && (
{syncDir && (
<Banner color="notice" className="flex flex-col gap-1.5">
<p>Directory is not empty. Do you want to open it instead?</p>
<div>
@@ -31,7 +31,7 @@ export function SyncToFilesystemSetting({
size="xs"
type="button"
onClick={() => {
openWorkspaceFromSyncDir.mutate(isNonEmpty);
openWorkspaceFromSyncDir.mutate(syncDir);
onCreateNewWorkspace();
}}
>
@@ -52,12 +52,12 @@ export function SyncToFilesystemSetting({
if (filePath != null) {
const files = await readDir(filePath);
if (files.length > 0) {
setIsNonEmpty(filePath);
setSyncDir(filePath);
return;
}
}
setIsNonEmpty(null);
setSyncDir(null);
onChange({ ...value, filePath });
}}
/>

View File

@@ -1,7 +1,7 @@
import { enableEncryption, revealWorkspaceKey, setWorkspaceKey } from '@yaakapp-internal/crypto';
import type { WorkspaceMeta } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { useAtomValue } from 'jotai/index';
import { useAtomValue } from 'jotai';
import { useEffect, useState } from 'react';
import { activeWorkspaceAtom, activeWorkspaceMetaAtom } from '../hooks/useActiveWorkspace';
import { createFastMutation } from '../hooks/useFastMutation';

View File

@@ -1,5 +1,5 @@
import classNames from 'classnames';
import { useAtomValue } from 'jotai/index';
import { useAtomValue } from 'jotai';
import React, { memo } from 'react';
import { activeWorkspaceAtom, activeWorkspaceMetaAtom } from '../hooks/useActiveWorkspace';
import { useToggleCommandPalette } from '../hooks/useToggleCommandPalette';

View File

@@ -1,5 +1,5 @@
import { patchModel, workspaceMetasAtom, workspacesAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai/index';
import { useAtomValue } from 'jotai';
import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm';
import { router } from '../lib/router';
import { Banner } from './core/Banner';

View File

@@ -6,23 +6,25 @@ interface Props {
}
export function HttpResponseDurationTag({ response }: Props) {
const [fallbackDuration, setFallbackDuration] = useState<number>(0);
const [fallbackElapsed, setFallbackElapsed] = useState<number>(0);
const timeout = useRef<NodeJS.Timeout>();
// Calculate the duration of the response for use when the response hasn't finished yet
useEffect(() => {
clearInterval(timeout.current);
timeout.current = setInterval(() => {
setFallbackDuration(Date.now() - new Date(response.createdAt + 'Z').getTime());
setFallbackElapsed(Date.now() - new Date(response.createdAt + 'Z').getTime());
}, 100);
return () => clearInterval(timeout.current);
}, [response.createdAt, response.elapsed, response.state]);
const title = `HEADER: ${formatMillis(response.elapsedHeaders)}\nTOTAL: ${formatMillis(response.elapsed)}`;
const elapsed = response.state === 'closed' ? response.elapsed : fallbackElapsed;
return (
<span className="font-mono" title={title}>
{formatMillis(response.elapsed || fallbackDuration)}
{formatMillis(elapsed)}
</span>
);
}

View File

@@ -16,7 +16,7 @@ export function BinaryViewer({ response }: Props) {
const contentType = getContentTypeFromHeaders(response.headers) ?? 'unknown';
// Wait until the response has been fully-downloaded
if (response.state === 'closed') {
if (response.state !== 'closed') {
return (
<EmptyStateText>
<LoadingIcon size="sm" />

View File

@@ -2,7 +2,7 @@ import type { HttpResponse } from '@yaakapp-internal/models';
import { useResponseBodyText } from '../../hooks/useResponseBodyText';
import { languageFromContentType } from '../../lib/contentType';
import { getContentTypeFromHeaders } from '../../lib/model_util';
import { BinaryViewer } from './BinaryViewer';
import { EmptyStateText } from '../EmptyStateText';
import { TextViewer } from './TextViewer';
import { WebPageViewer } from './WebPageViewer';
@@ -21,13 +21,10 @@ export function HTMLOrTextViewer({ response, pretty, textViewerClassName }: Prop
return null;
}
// Wasn't able to decode as text, so it must be binary
if (rawTextBody.data == null) {
return <BinaryViewer response={response} />;
}
if (language === 'html' && pretty) {
return <WebPageViewer response={response} />;
} else if (rawTextBody.data == null) {
return <EmptyStateText>Empty response</EmptyStateText>
} else {
return (
<TextViewer

View File

@@ -320,7 +320,7 @@ export function Sidebar({ className }: Props) {
'h-full grid grid-rows-[minmax(0,1fr)_auto]',
)}
>
<div className="pb-3 overflow-x-visible overflow-y-scroll hide-scrollbars pt-2">
<div className="pb-3 overflow-x-visible overflow-y-scroll pt-2">
<ContextMenu
triggerPosition={showMainContextMenu}
items={mainContextMenuItems}

View File

@@ -4,7 +4,7 @@ import {
websocketConnectionsAtom,
} from '@yaakapp-internal/models';
import classNames from 'classnames';
import { useAtomValue } from 'jotai/index';
import { useAtomValue } from 'jotai';
import React, { Fragment, memo } from 'react';
import { VStack } from '../core/Stacks';
import { DropMarker } from '../DropMarker';

View File

@@ -1,7 +1,7 @@
import { useSearch } from '@tanstack/react-router';
import type { CookieJar } from '@yaakapp-internal/models';
import { cookieJarsAtom } from '@yaakapp-internal/models';
import { atom, useAtomValue } from 'jotai/index';
import { atom, useAtomValue } from 'jotai';
import { useEffect } from 'react';
import { jotaiStore } from '../lib/jotai';
import { setWorkspaceSearchParams } from '../lib/setWorkspaceSearchParams';

View File

@@ -1,8 +1,7 @@
import { useSearch } from '@tanstack/react-router';
import type { Environment } from '@yaakapp-internal/models';
import { environmentsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { atom } from 'jotai/index';
import { useAtomValue , atom } from 'jotai';
import { useEffect } from 'react';
import { jotaiStore } from '../lib/jotai';

View File

@@ -1,6 +1,6 @@
import { useParams } from '@tanstack/react-router';
import { workspaceMetasAtom, workspacesAtom } from '@yaakapp-internal/models';
import { atom } from 'jotai/index';
import { atom } from 'jotai';
import { useEffect } from 'react';
import { jotaiStore } from '../lib/jotai';

View File

@@ -3,7 +3,7 @@ import {
httpRequestsAtom,
websocketRequestsAtom,
} from '@yaakapp-internal/models';
import { atom, useAtomValue } from 'jotai/index';
import { atom, useAtomValue } from 'jotai';
export const allRequestsAtom = atom(function (get) {
return [...get(httpRequestsAtom), ...get(grpcRequestsAtom), ...get(websocketRequestsAtom)];

View File

@@ -3,7 +3,7 @@ import {
httpResponsesAtom,
websocketConnectionsAtom,
} from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai/index';
import { useAtomValue } from 'jotai';
import { showAlert } from '../lib/alert';
import { showConfirmDelete } from '../lib/confirm';
import { jotaiStore } from '../lib/jotai';

View File

@@ -1,5 +1,5 @@
import { environmentsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai/index';
import { useAtomValue } from 'jotai';
import { useMemo } from 'react';
export function useEnvironmentsBreakdown() {

View File

@@ -1,7 +1,6 @@
import { useQuery } from '@tanstack/react-query';
import type { GetHttpAuthenticationSummaryResponse } from '@yaakapp-internal/plugins';
import { useAtomValue } from 'jotai';
import { atom } from 'jotai/index';
import { useAtomValue , atom } from 'jotai';
import { useState } from 'react';
import { jotaiStore } from '../lib/jotai';
import { invokeCmd } from '../lib/tauri';

View File

@@ -1,6 +1,6 @@
import type { GrpcConnection} from '@yaakapp-internal/models';
import { grpcConnectionsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai/index';
import { useAtomValue } from 'jotai';
export function useLatestGrpcConnection(requestId: string | null): GrpcConnection | null {
return useAtomValue(grpcConnectionsAtom).find((c) => c.requestId === requestId) ?? null;

View File

@@ -1,6 +1,6 @@
import type { HttpResponse} from '@yaakapp-internal/models';
import { httpResponsesAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai/index';
import { useAtomValue } from 'jotai';
export function useLatestHttpResponse(requestId: string | null): HttpResponse | null {
return useAtomValue(httpResponsesAtom).find((r) => r.requestId === requestId) ?? null;

View File

@@ -5,8 +5,7 @@ import {
grpcEventsAtom,
replaceModelsInStore,
} from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import { atom } from 'jotai/index';
import { useAtomValue , atom } from 'jotai';
import { useEffect } from 'react';
import { atomWithKVStorage } from '../lib/atoms/atomWithKVStorage';
import { activeRequestIdAtom } from './useActiveRequestId';

View File

@@ -1,6 +1,6 @@
import type { HttpResponse} from '@yaakapp-internal/models';
import { httpResponsesAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai/index';
import { useAtomValue } from 'jotai';
import { useKeyValue } from './useKeyValue';
import { useLatestHttpResponse } from './useLatestHttpResponse';

View File

@@ -5,7 +5,7 @@ import {
websocketConnectionsAtom,
websocketEventsAtom,
} from '@yaakapp-internal/models';
import { atom, useAtomValue } from 'jotai/index';
import { atom, useAtomValue } from 'jotai';
import { useEffect } from 'react';
import { atomWithKVStorage } from '../lib/atoms/atomWithKVStorage';
import { jotaiStore } from '../lib/jotai';

View File

@@ -1,6 +1,5 @@
import EventEmitter from 'eventemitter3';
import { atom } from 'jotai';
import { useAtom } from 'jotai/index';
import { atom , useAtom } from 'jotai';
import type { DependencyList } from 'react';
import { useCallback, useEffect } from 'react';

View File

@@ -3,7 +3,7 @@ import type { HttpResponse } from '@yaakapp-internal/models';
import { getResponseBodyText } from '../lib/responseBody';
export function useResponseBodyText(response: HttpResponse) {
return useQuery<string | null>({
return useQuery({
placeholderData: (prev) => prev, // Keep previous data on refetch
queryKey: ['response-body-text', response.id, response.updatedAt, response.contentLength],
queryFn: () => getResponseBodyText(response),

View File

@@ -1,7 +1,6 @@
import { useQuery } from '@tanstack/react-query';
import type { GetTemplateFunctionsResponse, TemplateFunction } from '@yaakapp-internal/plugins';
import { atom, useAtomValue } from 'jotai';
import { useSetAtom } from 'jotai/index';
import { atom, useAtomValue , useSetAtom } from 'jotai';
import { useMemo, useState } from 'react';
import type { TwigCompletionOption } from '../components/core/Editor/twig/completion';
import { invokeCmd } from '../lib/tauri';

View File

@@ -1,4 +1,4 @@
import { atom } from 'jotai/index';
import { atom } from 'jotai';
import { getKeyValue, setKeyValue } from '../keyValueStore';
export function atomWithKVStorage<T extends object | boolean | number | string | null>(

View File

@@ -1,4 +1,4 @@
import { atom } from 'jotai/index';
import { atom } from 'jotai';
import type { DialogInstance } from '../components/Dialogs';
import { jotaiStore } from './jotai';

View File

@@ -1,3 +1,3 @@
import { createStore } from 'jotai/index';
import { createStore } from 'jotai';
export const jotaiStore = createStore();

View File

@@ -5,18 +5,14 @@ import { getCharsetFromContentType } from './model_util';
import { invokeCmd } from './tauri';
export async function getResponseBodyText(response: HttpResponse): Promise<string | null> {
if (!response.bodyPath) return null;
if (!response.bodyPath) {
return null;
}
const bytes = await readFile(response.bodyPath);
const charset = getCharsetFromContentType(response.headers);
try {
return new TextDecoder(charset ?? 'utf-8', { fatal: true }).decode(bytes);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (err) {
// Failed to decode as text, so return null
return null;
}
return new TextDecoder(charset ?? 'utf-8', { fatal: false }).decode(bytes);
}
export async function getResponseBodyBlob(response: HttpResponse): Promise<Uint8Array | null> {

View File

@@ -1,4 +1,4 @@
import { atom } from 'jotai/index';
import { atom } from 'jotai';
import type { ToastInstance } from '../components/Toasts';
import { generateId } from './generateId';
import { jotaiStore } from './jotai';