mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-02-22 10:37:45 +01:00
Compare commits
11 Commits
v2025.2.0-
...
v2025.2.0-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb014b7c43 | ||
|
|
9fa0650647 | ||
|
|
b8c42677ca | ||
|
|
2eb3c2241c | ||
|
|
8fb7bbfe2e | ||
|
|
52eba74151 | ||
|
|
e651760713 | ||
|
|
82451a26f6 | ||
|
|
cc15f60fb6 | ||
|
|
2f8b2a81c7 | ||
|
|
6d4fdc91fe |
245
src-tauri/migrations/20250424152740_remove-fks.sql
Normal file
245
src-tauri/migrations/20250424152740_remove-fks.sql
Normal 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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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>•</span>
|
||||
<HttpResponseDurationTag
|
||||
response={activeResponse}
|
||||
/>
|
||||
<HttpResponseDurationTag response={activeResponse} />
|
||||
<span>•</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}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 });
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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)];
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { atom } from 'jotai/index';
|
||||
import { atom } from 'jotai';
|
||||
import type { DialogInstance } from '../components/Dialogs';
|
||||
import { jotaiStore } from './jotai';
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import { createStore } from 'jotai/index';
|
||||
import { createStore } from 'jotai';
|
||||
|
||||
export const jotaiStore = createStore();
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user