Start on plugin ctx API (#64)

This commit is contained in:
Gregory Schier
2024-08-14 06:42:54 -07:00
committed by GitHub
parent 5eef910b8c
commit c484dd4041
106 changed files with 1086 additions and 1219 deletions

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type GetHttpRequestByIdRequest = { id: string, };

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { HttpRequest } from "./HttpRequest";
export type GetHttpRequestByIdResponse = { httpRequest: HttpRequest | null, };

View File

@@ -6,7 +6,11 @@ import type { ExportHttpRequestRequest } from "./ExportHttpRequestRequest";
import type { ExportHttpRequestResponse } from "./ExportHttpRequestResponse";
import type { FilterRequest } from "./FilterRequest";
import type { FilterResponse } from "./FilterResponse";
import type { GetHttpRequestByIdRequest } from "./GetHttpRequestByIdRequest";
import type { GetHttpRequestByIdResponse } from "./GetHttpRequestByIdResponse";
import type { ImportRequest } from "./ImportRequest";
import type { ImportResponse } from "./ImportResponse";
import type { SendHttpRequestRequest } from "./SendHttpRequestRequest";
import type { SendHttpRequestResponse } from "./SendHttpRequestResponse";
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "empty_response" } & EmptyResponse;
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "empty_response" } & EmptyResponse;

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { HttpRequest } from "./HttpRequest";
export type SendHttpRequestRequest = { httpRequest: HttpRequest, };

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { HttpResponse } from "./HttpResponse";
export type SendHttpRequestResponse = { httpResponse: HttpResponse, };

View File

@@ -31,5 +31,10 @@ export * from './gen/InternalEvent';
export * from './gen/InternalEventPayload';
export * from './gen/KeyValue';
export * from './gen/Model';
export * from './gen/SendHttpRequestRequest';
export * from './gen/SendHttpRequestResponse';
export * from './gen/GetHttpRequestByIdRequest';
export * from './gen/GetHttpRequestByIdResponse';
export * from './gen/SendHttpRequestResponse';
export * from './gen/Settings';
export * from './gen/Workspace';

View File

@@ -1,12 +1,11 @@
import { HttpRequest } from '../gen/HttpRequest';
import { HttpResponse } from '../gen/HttpResponse';
import { GetHttpRequestByIdRequest } from '../gen/GetHttpRequestByIdRequest';
import { GetHttpRequestByIdResponse } from '../gen/GetHttpRequestByIdResponse';
import { SendHttpRequestRequest } from '../gen/SendHttpRequestRequest';
import { SendHttpRequestResponse } from '../gen/SendHttpRequestResponse';
export type YaakContext = {
metadata: {
getVersion(): Promise<string>;
};
httpRequest: {
send(id: string): Promise<HttpResponse>;
getById(id: string): Promise<HttpRequest | null>;
send(args: SendHttpRequestRequest): Promise<SendHttpRequestResponse['httpResponse']>;
getById(args: GetHttpRequestByIdRequest): Promise<GetHttpRequestByIdResponse['httpRequest']>;
};
};

View File

@@ -3,6 +3,7 @@ import { FilterPlugin } from './filter';
import { HttpRequestActionPlugin } from './httpRequestAction';
import { ImporterPlugin } from './import';
import { ThemePlugin } from './theme';
export { YaakContext } from './context';
/**
* The global structure of a Yaak plugin

View File

@@ -10,369 +10,10 @@ import * as _m0 from "protobufjs/minimal";
export const protobufPackage = "yaak.plugins.runtime";
export interface PluginInfo {
plugin: string;
}
export interface HookResponse {
info: PluginInfo | undefined;
data: string;
}
export interface HookImportRequest {
data: string;
}
export interface HookResponseFilterRequest {
filter: string;
body: string;
contentType: string;
}
export interface HookExportRequest {
request: string;
}
export interface EventStreamEvent {
event: string;
}
function createBasePluginInfo(): PluginInfo {
return { plugin: "" };
}
export const PluginInfo = {
encode(message: PluginInfo, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.plugin !== "") {
writer.uint32(10).string(message.plugin);
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): PluginInfo {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBasePluginInfo();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 10) {
break;
}
message.plugin = reader.string();
continue;
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(object: any): PluginInfo {
return { plugin: isSet(object.plugin) ? globalThis.String(object.plugin) : "" };
},
toJSON(message: PluginInfo): unknown {
const obj: any = {};
if (message.plugin !== "") {
obj.plugin = message.plugin;
}
return obj;
},
create(base?: DeepPartial<PluginInfo>): PluginInfo {
return PluginInfo.fromPartial(base ?? {});
},
fromPartial(object: DeepPartial<PluginInfo>): PluginInfo {
const message = createBasePluginInfo();
message.plugin = object.plugin ?? "";
return message;
},
};
function createBaseHookResponse(): HookResponse {
return { info: undefined, data: "" };
}
export const HookResponse = {
encode(message: HookResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.info !== undefined) {
PluginInfo.encode(message.info, writer.uint32(10).fork()).ldelim();
}
if (message.data !== "") {
writer.uint32(18).string(message.data);
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): HookResponse {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseHookResponse();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 10) {
break;
}
message.info = PluginInfo.decode(reader, reader.uint32());
continue;
case 2:
if (tag !== 18) {
break;
}
message.data = reader.string();
continue;
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(object: any): HookResponse {
return {
info: isSet(object.info) ? PluginInfo.fromJSON(object.info) : undefined,
data: isSet(object.data) ? globalThis.String(object.data) : "",
};
},
toJSON(message: HookResponse): unknown {
const obj: any = {};
if (message.info !== undefined) {
obj.info = PluginInfo.toJSON(message.info);
}
if (message.data !== "") {
obj.data = message.data;
}
return obj;
},
create(base?: DeepPartial<HookResponse>): HookResponse {
return HookResponse.fromPartial(base ?? {});
},
fromPartial(object: DeepPartial<HookResponse>): HookResponse {
const message = createBaseHookResponse();
message.info = (object.info !== undefined && object.info !== null)
? PluginInfo.fromPartial(object.info)
: undefined;
message.data = object.data ?? "";
return message;
},
};
function createBaseHookImportRequest(): HookImportRequest {
return { data: "" };
}
export const HookImportRequest = {
encode(message: HookImportRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.data !== "") {
writer.uint32(10).string(message.data);
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): HookImportRequest {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseHookImportRequest();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 10) {
break;
}
message.data = reader.string();
continue;
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(object: any): HookImportRequest {
return { data: isSet(object.data) ? globalThis.String(object.data) : "" };
},
toJSON(message: HookImportRequest): unknown {
const obj: any = {};
if (message.data !== "") {
obj.data = message.data;
}
return obj;
},
create(base?: DeepPartial<HookImportRequest>): HookImportRequest {
return HookImportRequest.fromPartial(base ?? {});
},
fromPartial(object: DeepPartial<HookImportRequest>): HookImportRequest {
const message = createBaseHookImportRequest();
message.data = object.data ?? "";
return message;
},
};
function createBaseHookResponseFilterRequest(): HookResponseFilterRequest {
return { filter: "", body: "", contentType: "" };
}
export const HookResponseFilterRequest = {
encode(message: HookResponseFilterRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.filter !== "") {
writer.uint32(10).string(message.filter);
}
if (message.body !== "") {
writer.uint32(18).string(message.body);
}
if (message.contentType !== "") {
writer.uint32(26).string(message.contentType);
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): HookResponseFilterRequest {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseHookResponseFilterRequest();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 10) {
break;
}
message.filter = reader.string();
continue;
case 2:
if (tag !== 18) {
break;
}
message.body = reader.string();
continue;
case 3:
if (tag !== 26) {
break;
}
message.contentType = reader.string();
continue;
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(object: any): HookResponseFilterRequest {
return {
filter: isSet(object.filter) ? globalThis.String(object.filter) : "",
body: isSet(object.body) ? globalThis.String(object.body) : "",
contentType: isSet(object.contentType) ? globalThis.String(object.contentType) : "",
};
},
toJSON(message: HookResponseFilterRequest): unknown {
const obj: any = {};
if (message.filter !== "") {
obj.filter = message.filter;
}
if (message.body !== "") {
obj.body = message.body;
}
if (message.contentType !== "") {
obj.contentType = message.contentType;
}
return obj;
},
create(base?: DeepPartial<HookResponseFilterRequest>): HookResponseFilterRequest {
return HookResponseFilterRequest.fromPartial(base ?? {});
},
fromPartial(object: DeepPartial<HookResponseFilterRequest>): HookResponseFilterRequest {
const message = createBaseHookResponseFilterRequest();
message.filter = object.filter ?? "";
message.body = object.body ?? "";
message.contentType = object.contentType ?? "";
return message;
},
};
function createBaseHookExportRequest(): HookExportRequest {
return { request: "" };
}
export const HookExportRequest = {
encode(message: HookExportRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.request !== "") {
writer.uint32(10).string(message.request);
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): HookExportRequest {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseHookExportRequest();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 10) {
break;
}
message.request = reader.string();
continue;
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(object: any): HookExportRequest {
return { request: isSet(object.request) ? globalThis.String(object.request) : "" };
},
toJSON(message: HookExportRequest): unknown {
const obj: any = {};
if (message.request !== "") {
obj.request = message.request;
}
return obj;
},
create(base?: DeepPartial<HookExportRequest>): HookExportRequest {
return HookExportRequest.fromPartial(base ?? {});
},
fromPartial(object: DeepPartial<HookExportRequest>): HookExportRequest {
const message = createBaseHookExportRequest();
message.request = object.request ?? "";
return message;
},
};
function createBaseEventStreamEvent(): EventStreamEvent {
return { event: "" };
}

View File

@@ -1,4 +1,11 @@
import { ImportResponse, InternalEvent, InternalEventPayload } from '@yaakapp/api';
import {
GetHttpRequestByIdResponse,
ImportResponse,
InternalEvent,
InternalEventPayload,
SendHttpRequestResponse,
} from '@yaakapp/api';
import { YaakContext } from '@yaakapp/api/lib/plugins/context';
import interceptStdout from 'intercept-stdout';
import * as console from 'node:console';
import { readFileSync } from 'node:fs';
@@ -7,7 +14,7 @@ import * as util from 'node:util';
import { parentPort, workerData } from 'node:worker_threads';
new Promise<void>(async (resolve, reject) => {
const { pluginDir /*, pluginRefId*/ } = workerData;
const { pluginDir, pluginRefId } = workerData;
const pathPkg = path.join(pluginDir, 'package.json');
// NOTE: Use POSIX join because require() needs forward slash
@@ -33,8 +40,64 @@ new Promise<void>(async (resolve, reject) => {
console.log('Plugin initialized', pkg.name, capabilities, Object.keys(mod));
function buildEventToSend(
payload: InternalEventPayload,
replyId: string | null = null,
): InternalEvent {
return { pluginRefId, id: genId(), replyId, payload };
}
function sendPayload(payload: InternalEventPayload, replyId: string | null = null): string {
const event = buildEventToSend(payload, replyId);
sendEvent(event);
return event.id;
}
function sendEvent(event: InternalEvent) {
parentPort!.postMessage(event);
}
async function sendAndWaitForReply<T extends Omit<InternalEventPayload, 'type'>>(
payload: InternalEventPayload,
): Promise<T> {
// 1. Build event to send
const eventToSend = buildEventToSend(payload, null);
// 2. Spawn listener in background
const promise = new Promise<InternalEventPayload>(async (resolve) => {
const cb = (event: InternalEvent) => {
if (event.replyId === eventToSend.id) {
resolve(event.payload); // Not type-safe but oh well
parentPort!.off('message', cb); // Unlisten, now that we're done
}
};
parentPort!.on('message', cb);
});
// 3. Send the event after we start listening (to prevent race)
sendEvent(eventToSend);
// 4. Return the listener promise
return promise as unknown as Promise<T>;
}
const ctx: YaakContext = {
httpRequest: {
async getById({ id }) {
const payload = { type: 'get_http_request_by_id_request', id } as const;
const { httpRequest } = await sendAndWaitForReply<GetHttpRequestByIdResponse>(payload);
return httpRequest;
},
async send({ httpRequest }) {
const payload = { type: 'send_http_request_request', httpRequest } as const;
const { httpResponse } = await sendAndWaitForReply<SendHttpRequestResponse>(payload);
return httpResponse;
},
},
};
// Message comes into the plugin to be processed
parentPort!.on('message', async ({ payload, pluginRefId, id: replyId }: InternalEvent) => {
parentPort!.on('message', async ({ payload, id: replyId }: InternalEvent) => {
console.log(`Received ${payload.type}`);
try {
@@ -45,18 +108,18 @@ new Promise<void>(async (resolve, reject) => {
version: pkg.version,
capabilities,
};
sendToServer({ id: genId(), pluginRefId, replyId, payload });
sendPayload(payload, replyId);
return;
}
if (payload.type === 'import_request' && typeof mod.pluginHookImport === 'function') {
const reply: ImportResponse | null = await mod.pluginHookImport({}, payload.content);
const reply: ImportResponse | null = await mod.pluginHookImport(ctx, payload.content);
if (reply != null) {
const replyPayload: InternalEventPayload = {
type: 'import_response',
resources: reply?.resources,
};
sendToServer({ id: genId(), pluginRefId, replyId, payload: replyPayload });
sendPayload(replyPayload, replyId);
return;
} else {
// Continue, to send back an empty reply
@@ -67,25 +130,25 @@ new Promise<void>(async (resolve, reject) => {
payload.type === 'export_http_request_request' &&
typeof mod.pluginHookExport === 'function'
) {
const reply: string = await mod.pluginHookExport({}, payload.httpRequest);
const reply: string = await mod.pluginHookExport(ctx, payload.httpRequest);
const replyPayload: InternalEventPayload = {
type: 'export_http_request_response',
content: reply,
};
sendToServer({ id: genId(), pluginRefId, replyId, payload: replyPayload });
sendPayload(replyPayload, replyId);
return;
}
if (payload.type === 'filter_request' && typeof mod.pluginHookResponseFilter === 'function') {
const reply: string = await mod.pluginHookResponseFilter(
{},
{ filter: payload.filter, body: payload.content },
);
const reply: string = await mod.pluginHookResponseFilter(ctx, {
filter: payload.filter,
body: payload.content,
});
const replyPayload: InternalEventPayload = {
type: 'filter_response',
content: reply,
};
sendToServer({ id: genId(), pluginRefId, replyId, payload: replyPayload });
sendPayload(replyPayload, replyId);
return;
}
} catch (err) {
@@ -94,9 +157,7 @@ new Promise<void>(async (resolve, reject) => {
}
// No matches, so send back an empty response so the caller doesn't block forever
const id = genId();
console.log('Sending nothing back to', id, { replyId });
sendToServer({ id, pluginRefId, replyId, payload: { type: 'empty_response' } });
sendPayload({ type: 'empty_response' }, replyId);
});
resolve();
@@ -104,10 +165,6 @@ new Promise<void>(async (resolve, reject) => {
console.log('failed to boot plugin', err);
});
function sendToServer(e: InternalEvent) {
parentPort!.postMessage(e);
}
function genId(len = 5): string {
const alphabet = '01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
let id = '';

66
src-tauri/Cargo.lock generated
View File

@@ -2052,30 +2052,6 @@ dependencies = [
"system-deps",
]
[[package]]
name = "grpc"
version = "0.1.0"
dependencies = [
"anyhow",
"dunce",
"hyper 0.14.30",
"hyper-rustls 0.24.2",
"log",
"md5",
"prost 0.12.6",
"prost-reflect",
"prost-types 0.12.6",
"serde",
"serde_json",
"tauri",
"tauri-plugin-shell",
"tokio",
"tokio-stream",
"tonic 0.10.2",
"tonic-reflection",
"uuid",
]
[[package]]
name = "gtk"
version = "0.18.1"
@@ -6125,13 +6101,6 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "templates"
version = "0.1.0"
dependencies = [
"log",
]
[[package]]
name = "tendril"
version = "0.4.3"
@@ -7573,7 +7542,6 @@ dependencies = [
"chrono",
"cocoa",
"datetime",
"grpc",
"hex_color",
"http 1.1.0",
"log",
@@ -7597,13 +7565,38 @@ dependencies = [
"tauri-plugin-shell",
"tauri-plugin-updater",
"tauri-plugin-window-state",
"templates",
"thiserror",
"tokio",
"tokio-stream",
"uuid",
"yaak_grpc",
"yaak_models",
"yaak_plugin_runtime",
"yaak_templates",
]
[[package]]
name = "yaak_grpc"
version = "0.1.0"
dependencies = [
"anyhow",
"dunce",
"hyper 0.14.30",
"hyper-rustls 0.24.2",
"log",
"md5",
"prost 0.12.6",
"prost-reflect",
"prost-types 0.12.6",
"serde",
"serde_json",
"tauri",
"tauri-plugin-shell",
"tokio",
"tokio-stream",
"tonic 0.10.2",
"tonic-reflection",
"uuid",
]
[[package]]
@@ -7651,6 +7644,13 @@ dependencies = [
"yaak_models",
]
[[package]]
name = "yaak_templates"
version = "0.1.0"
dependencies = [
"log",
]
[[package]]
name = "zbus"
version = "4.0.1"

View File

@@ -1,5 +1,5 @@
[workspace]
members = ["grpc", "templates", "yaak_plugin_runtime", "yaak_models"]
members = ["yaak_grpc", "yaak_templates", "yaak_plugin_runtime", "yaak_models"]
[package]
@@ -26,8 +26,8 @@ cocoa = "0.25.0"
openssl-sys = { version = "0.9", features = ["vendored"] } # For Ubuntu installation to work
[dependencies]
grpc = { path = "./grpc" }
templates = { path = "./templates" }
yaak_grpc = { path = "yaak_grpc" }
yaak_templates = { path = "yaak_templates" }
yaak_plugin_runtime = { path = "yaak_plugin_runtime" }
anyhow = "1.0.86"
base64 = "0.22.0"
@@ -43,7 +43,7 @@ reqwest_cookie_store = "0.8.0"
serde = { version = "1.0.198", features = ["derive"] }
serde_json = { version = "1.0.116", features = ["raw_value"] }
serde_yaml = "0.9.34"
tauri = { workspace = true }
tauri = { workspace = true, features = ["unstable"] }
tauri-plugin-shell = { workspace = true }
tauri-plugin-clipboard-manager = "2.0.0-rc.0"
tauri-plugin-dialog = "2.0.0-rc.0"

View File

@@ -0,0 +1,2 @@
ALTER TABLE environments DROP COLUMN model;
ALTER TABLE environments ADD COLUMN model TEXT DEFAULT 'environment';

View File

@@ -3,9 +3,11 @@ use std::fmt::Display;
use log::{debug, info};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use tauri::{AppHandle, Manager};
use tauri::{Manager, Runtime, WebviewWindow};
use yaak_models::queries::{generate_id, get_key_value_int, get_key_value_string, set_key_value_int, set_key_value_string};
use yaak_models::queries::{
generate_id, get_key_value_int, get_key_value_string, set_key_value_int, set_key_value_string,
};
use crate::is_dev;
@@ -36,7 +38,7 @@ pub enum AnalyticsResource {
impl AnalyticsResource {
pub fn from_str(s: &str) -> serde_json::Result<AnalyticsResource> {
return serde_json::from_str(format!("\"{s}\"").as_str());
serde_json::from_str(format!("\"{s}\"").as_str())
}
}
@@ -74,7 +76,7 @@ pub enum AnalyticsAction {
impl AnalyticsAction {
pub fn from_str(s: &str) -> serde_json::Result<AnalyticsAction> {
return serde_json::from_str(format!("\"{s}\"").as_str());
serde_json::from_str(format!("\"{s}\"").as_str())
}
}
@@ -96,19 +98,18 @@ pub struct LaunchEventInfo {
pub num_launches: i32,
}
pub async fn track_launch_event(app: &AppHandle) -> LaunchEventInfo {
pub async fn track_launch_event<R: Runtime>(w: &WebviewWindow<R>) -> LaunchEventInfo {
let last_tracked_version_key = "last_tracked_version";
let mut info = LaunchEventInfo::default();
info.num_launches = get_num_launches(app).await + 1;
info.previous_version =
get_key_value_string(app, NAMESPACE, last_tracked_version_key, "").await;
info.current_version = app.package_info().version.to_string();
info.num_launches = get_num_launches(w).await + 1;
info.previous_version = get_key_value_string(w, NAMESPACE, last_tracked_version_key, "").await;
info.current_version = w.package_info().version.to_string();
if info.previous_version.is_empty() {
track_event(
app,
w,
AnalyticsResource::App,
AnalyticsAction::LaunchFirst,
None,
@@ -118,7 +119,7 @@ pub async fn track_launch_event(app: &AppHandle) -> LaunchEventInfo {
info.launched_after_update = info.current_version != info.previous_version;
if info.launched_after_update {
track_event(
app,
w,
AnalyticsResource::App,
AnalyticsAction::LaunchUpdate,
Some(json!({ NUM_LAUNCHES_KEY: info.num_launches })),
@@ -129,7 +130,7 @@ pub async fn track_launch_event(app: &AppHandle) -> LaunchEventInfo {
// Track a launch event in all cases
track_event(
app,
w,
AnalyticsResource::App,
AnalyticsAction::Launch,
Some(json!({ NUM_LAUNCHES_KEY: info.num_launches })),
@@ -139,27 +140,27 @@ pub async fn track_launch_event(app: &AppHandle) -> LaunchEventInfo {
// Update key values
set_key_value_string(
app,
w,
NAMESPACE,
last_tracked_version_key,
info.current_version.as_str(),
)
.await;
set_key_value_int(app, NAMESPACE, NUM_LAUNCHES_KEY, info.num_launches).await;
set_key_value_int(w, NAMESPACE, NUM_LAUNCHES_KEY, info.num_launches).await;
info
}
pub async fn track_event(
app_handle: &AppHandle,
pub async fn track_event<R: Runtime>(
w: &WebviewWindow<R>,
resource: AnalyticsResource,
action: AnalyticsAction,
attributes: Option<Value>,
) {
let id = get_id(app_handle).await;
let id = get_id(w).await;
let event = format!("{}.{}", resource, action);
let attributes_json = attributes.unwrap_or("{}".to_string().into()).to_string();
let info = app_handle.package_info();
let info = w.app_handle().package_info();
let tz = datetime::sys_timezone().unwrap_or("unknown".to_string());
let site = match is_dev() {
true => "site_TkHWjoXwZPq3HfhERb",
@@ -177,7 +178,7 @@ pub async fn track_event(
("v", info.version.clone().to_string()),
("os", get_os().to_string()),
("tz", tz),
("xy", get_window_size(app_handle)),
("xy", get_window_size(w)),
];
let req = reqwest::Client::builder()
.build()
@@ -208,13 +209,8 @@ fn get_os() -> &'static str {
}
}
fn get_window_size(app_handle: &AppHandle) -> String {
let window = match app_handle.webview_windows().into_values().next() {
Some(w) => w,
None => return "unknown".to_string(),
};
let current_monitor = match window.current_monitor() {
fn get_window_size<R: Runtime>(w: &WebviewWindow<R>) -> String {
let current_monitor = match w.current_monitor() {
Ok(Some(m)) => m,
_ => return "unknown".to_string(),
};
@@ -231,17 +227,17 @@ fn get_window_size(app_handle: &AppHandle) -> String {
)
}
async fn get_id(app_handle: &AppHandle) -> String {
let id = get_key_value_string(app_handle, "analytics", "id", "").await;
async fn get_id<R: Runtime>(w: &WebviewWindow<R>) -> String {
let id = get_key_value_string(w, "analytics", "id", "").await;
if id.is_empty() {
let new_id = generate_id();
set_key_value_string(app_handle, "analytics", "id", new_id.as_str()).await;
set_key_value_string(w, "analytics", "id", new_id.as_str()).await;
new_id
} else {
id
}
}
pub async fn get_num_launches(app: &AppHandle) -> i32 {
get_key_value_int(app, NAMESPACE, NUM_LAUNCHES_KEY, 0).await
pub async fn get_num_launches<R: Runtime>(w: &WebviewWindow<R>) -> i32 {
get_key_value_int(w, NAMESPACE, NUM_LAUNCHES_KEY, 0).await
}

View File

@@ -2,7 +2,7 @@ use std::collections::HashMap;
use KeyAndValueRef::{Ascii, Binary};
use grpc::{KeyAndValueRef, MetadataMap};
use yaak_grpc::{KeyAndValueRef, MetadataMap};
pub fn metadata_to_map(metadata: MetadataMap) -> HashMap<String, String> {
let mut entries = HashMap::new();

View File

@@ -11,24 +11,25 @@ use crate::{render, response_err};
use base64::Engine;
use http::header::{ACCEPT, USER_AGENT};
use http::{HeaderMap, HeaderName, HeaderValue};
use log::{error, info, warn};
use log::{error, warn};
use mime_guess::Mime;
use reqwest::redirect::Policy;
use reqwest::Method;
use reqwest::{multipart, Url};
use tauri::{Manager, WebviewWindow};
use tauri::{Manager, Runtime, WebviewWindow};
use tokio::sync::oneshot;
use tokio::sync::watch::Receiver;
use yaak_models::models::{Cookie, CookieJar, Environment, HttpRequest, HttpResponse, HttpResponseHeader};
use yaak_models::models::{
Cookie, CookieJar, Environment, HttpRequest, HttpResponse, HttpResponseHeader,
};
use yaak_models::queries::{get_workspace, update_response_if_id, upsert_cookie_jar};
pub async fn send_http_request(
window: &WebviewWindow,
pub async fn send_http_request<R: Runtime>(
window: &WebviewWindow<R>,
request: HttpRequest,
response: &HttpResponse,
environment: Option<Environment>,
cookie_jar: Option<CookieJar>,
download_path: Option<PathBuf>,
cancel_rx: &mut Receiver<bool>,
) -> Result<HttpResponse, String> {
let environment_ref = environment.as_ref();
@@ -442,16 +443,6 @@ pub async fn send_http_request(
.await
.expect("Failed to update response");
// Copy response to the download path, if specified
match (download_path, response.body_path.clone()) {
(Some(dl_path), Some(body_path)) => {
info!("Downloading response body to {}", dl_path.display());
fs::copy(body_path, dl_path)
.expect("Failed to copy file for response download");
}
_ => {}
};
// Add cookie store if specified
if let Some((cookie_store, mut cookie_jar)) = maybe_cookie_manager {
// let cookies = response_headers.get_all(SET_COOKIE).iter().map(|h| {
@@ -466,7 +457,8 @@ pub async fn send_http_request(
.unwrap()
.iter_any()
.map(|c| {
let json_cookie = serde_json::to_value(&c).expect("Failed to serialize cookie");
let json_cookie =
serde_json::to_value(&c).expect("Failed to serialize cookie");
serde_json::from_value(json_cookie).expect("Failed to deserialize cookie")
})
.collect::<Vec<_>>();

View File

@@ -16,17 +16,17 @@ use fern::colors::ColoredLevelConfig;
use log::{debug, error, info, warn};
use rand::random;
use serde_json::{json, Value};
use tauri::Listener;
#[cfg(target_os = "macos")]
use tauri::TitleBarStyle;
use tauri::{AppHandle, Emitter, LogicalSize, RunEvent, State, WebviewUrl, WebviewWindow};
use tauri::{Listener, Runtime};
use tauri::{Manager, WindowEvent};
use tauri_plugin_log::{fern, Target, TargetKind};
use tauri_plugin_shell::ShellExt;
use tokio::sync::Mutex;
use tokio::sync::{watch, Mutex};
use ::grpc::manager::{DynamicMessage, GrpcHandle};
use ::grpc::{deserialize_message, serialize_message, Code, ServiceDefinition};
use yaak_grpc::manager::{DynamicMessage, GrpcHandle};
use yaak_grpc::{deserialize_message, serialize_message, Code, ServiceDefinition};
use yaak_plugin_runtime::manager::PluginManager;
use crate::analytics::{AnalyticsAction, AnalyticsResource};
@@ -42,7 +42,7 @@ use yaak_models::models::{
GrpcRequest, HttpRequest, HttpResponse, KeyValue, ModelType, Settings, Workspace,
};
use yaak_models::queries::{
cancel_pending_grpc_connections, cancel_pending_responses, create_http_response,
cancel_pending_grpc_connections, cancel_pending_responses, create_default_http_response,
delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar, delete_environment,
delete_folder, delete_grpc_connection, delete_grpc_request, delete_http_request,
delete_http_response, delete_workspace, duplicate_grpc_request, duplicate_http_request,
@@ -54,7 +54,10 @@ use yaak_models::queries::{
upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection,
upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_workspace,
};
use yaak_plugin_runtime::events::FilterResponse;
use yaak_plugin_runtime::events::{
FilterResponse, GetHttpRequestByIdResponse, InternalEvent, InternalEventPayload,
SendHttpRequestResponse,
};
mod analytics;
mod export_resources;
@@ -96,11 +99,15 @@ async fn cmd_metadata(app_handle: AppHandle) -> Result<AppMetaData, ()> {
#[tauri::command]
async fn cmd_dismiss_notification(
app: AppHandle,
window: WebviewWindow,
notification_id: &str,
yaak_notifier: State<'_, Mutex<YaakNotifier>>,
) -> Result<(), String> {
yaak_notifier.lock().await.seen(&app, notification_id).await
yaak_notifier
.lock()
.await
.seen(&window, notification_id)
.await
}
#[tauri::command]
@@ -135,17 +142,21 @@ async fn cmd_grpc_go(
request_id: &str,
environment_id: Option<&str>,
proto_files: Vec<String>,
w: WebviewWindow,
window: WebviewWindow,
grpc_handle: State<'_, Mutex<GrpcHandle>>,
) -> Result<String, String> {
let req = get_grpc_request(&w, request_id)
let req = get_grpc_request(&window, request_id)
.await
.map_err(|e| e.to_string())?;
let environment = match environment_id {
Some(id) => Some(get_environment(&w, id).await.map_err(|e| e.to_string())?),
Some(id) => Some(
get_environment(&window, id)
.await
.map_err(|e| e.to_string())?,
),
None => None,
};
let workspace = get_workspace(&w, &req.workspace_id)
let workspace = get_workspace(&window, &req.workspace_id)
.await
.map_err(|e| e.to_string())?;
let mut metadata = HashMap::new();
@@ -199,7 +210,7 @@ async fn cmd_grpc_go(
let conn = {
let req = req.clone();
upsert_grpc_connection(
&w,
&window,
&GrpcConnection {
workspace_id: req.workspace_id,
request_id: req.id,
@@ -256,7 +267,7 @@ async fn cmd_grpc_go(
Ok(c) => c,
Err(err) => {
upsert_grpc_connection(
&w,
&window,
&GrpcConnection {
elapsed: start.elapsed().as_millis() as i32,
error: Some(err.clone()),
@@ -282,7 +293,7 @@ async fn cmd_grpc_go(
let cb = {
let cancelled_rx = cancelled_rx.clone();
let w = w.clone();
let w = window.clone();
let base_msg = base_msg.clone();
let method_desc = method_desc.clone();
let vars = vars.clone();
@@ -355,10 +366,10 @@ async fn cmd_grpc_go(
}
}
};
let event_handler = w.listen_any(format!("grpc_client_msg_{}", conn.id).as_str(), cb);
let event_handler = window.listen_any(format!("grpc_client_msg_{}", conn.id).as_str(), cb);
let grpc_listen = {
let w = w.clone();
let w = window.clone();
let base_event = base_msg.clone();
let req = req.clone();
let vars = vars.clone();
@@ -603,7 +614,7 @@ async fn cmd_grpc_go(
{
let conn_id = conn_id.clone();
tauri::async_runtime::spawn(async move {
let w = w.clone();
let w = window.clone();
tokio::select! {
_ = grpc_listen => {
let events = list_grpc_events(&w, &conn_id)
@@ -691,7 +702,6 @@ async fn cmd_send_ephemeral_request(
&response,
environment,
cookie_jar,
None,
&mut cancel_rx,
)
.await
@@ -701,7 +711,7 @@ async fn cmd_send_ephemeral_request(
async fn cmd_filter_response(
w: WebviewWindow,
response_id: &str,
plugin_manager: State<'_, Mutex<PluginManager>>,
plugin_manager: State<'_, PluginManager>,
filter: &str,
) -> Result<FilterResponse, String> {
let response = get_http_response(&w, response_id)
@@ -724,8 +734,6 @@ async fn cmd_filter_response(
// TODO: Have plugins register their own content type (regex?)
plugin_manager
.lock()
.await
.run_filter(filter, &body, &content_type)
.await
.map_err(|e| e.to_string())
@@ -734,15 +742,13 @@ async fn cmd_filter_response(
#[tauri::command]
async fn cmd_import_data(
w: WebviewWindow,
plugin_manager: State<'_, Mutex<PluginManager>>,
plugin_manager: State<'_, PluginManager>,
file_path: &str,
) -> Result<WorkspaceExportResources, String> {
let file =
read_to_string(file_path).unwrap_or_else(|_| panic!("Unable to read file {}", file_path));
let file_contents = file.as_str();
let (import_result, plugin_name) = plugin_manager
.lock()
.await
.run_import(file_contents)
.await
.map_err(|e| e.to_string())?;
@@ -853,7 +859,7 @@ async fn cmd_import_data(
);
analytics::track_event(
&w.app_handle(),
&w,
AnalyticsResource::App,
AnalyticsAction::Import,
Some(json!({ "plugin": plugin_name })),
@@ -867,7 +873,7 @@ async fn cmd_import_data(
async fn cmd_request_to_curl(
app: AppHandle,
request_id: &str,
plugin_manager: State<'_, Mutex<PluginManager>>,
plugin_manager: State<'_, PluginManager>,
environment_id: Option<&str>,
) -> Result<String, String> {
let request = get_http_request(&app, request_id)
@@ -883,8 +889,6 @@ async fn cmd_request_to_curl(
let rendered = render_request(&request, &workspace, environment.as_ref());
let import_response = plugin_manager
.lock()
.await
.run_export_curl(&rendered)
.await
.map_err(|e| e.to_string())?;
@@ -894,19 +898,19 @@ async fn cmd_request_to_curl(
#[tauri::command]
async fn cmd_curl_to_request(
command: &str,
plugin_manager: State<'_, Mutex<PluginManager>>,
plugin_manager: State<'_, PluginManager>,
workspace_id: &str,
w: WebviewWindow,
) -> Result<HttpRequest, String> {
let (import_result, plugin_name) = plugin_manager
.lock()
.await
.run_import(command)
.await
.map_err(|e| e.to_string())?;
let (import_result, plugin_name) = {
plugin_manager
.run_import(command)
.await
.map_err(|e| e.to_string())?
};
analytics::track_event(
&w.app_handle(),
&w,
AnalyticsResource::App,
AnalyticsAction::Import,
Some(json!({ "plugin": plugin_name })),
@@ -947,7 +951,7 @@ async fn cmd_export_data(
f.sync_all().expect("Failed to sync");
analytics::track_event(
&window.app_handle(),
&window,
AnalyticsResource::App,
AnalyticsAction::Export,
None,
@@ -984,7 +988,6 @@ async fn cmd_send_http_request(
window: WebviewWindow,
environment_id: Option<&str>,
cookie_jar_id: Option<&str>,
download_dir: Option<&str>,
// NOTE: We receive the entire request because to account for the race
// condition where the user may have just edited a field before sending
// that has not yet been saved in the DB.
@@ -1010,28 +1013,9 @@ async fn cmd_send_http_request(
None => None,
};
let response = create_http_response(
&window,
&request.id,
0,
0,
"",
0,
None,
None,
None,
vec![],
None,
None,
)
.await
.expect("Failed to create response");
let download_path = if let Some(p) = download_dir {
Some(std::path::Path::new(p).to_path_buf())
} else {
None
};
let response = create_default_http_response(&window, &request.id)
.await
.map_err(|e| e.to_string())?;
let (cancel_tx, mut cancel_rx) = tokio::sync::watch::channel(false);
window.listen_any(
@@ -1047,16 +1031,15 @@ async fn cmd_send_http_request(
&response,
environment,
cookie_jar,
download_path,
&mut cancel_rx,
)
.await
}
async fn response_err(
async fn response_err<R: Runtime>(
response: &HttpResponse,
error: String,
w: &WebviewWindow,
w: &WebviewWindow<R>,
) -> Result<HttpResponse, String> {
warn!("Failed to send request: {}", error);
let mut response = response.clone();
@@ -1080,7 +1063,7 @@ async fn cmd_track_event(
AnalyticsAction::from_str(action),
) {
(Ok(resource), Ok(action)) => {
analytics::track_event(&window.app_handle(), resource, action, attributes).await;
analytics::track_event(&window, resource, action, attributes).await;
}
(r, a) => {
error!(
@@ -1635,9 +1618,8 @@ pub fn run() {
let grpc_handle = GrpcHandle::new(&app.app_handle());
app.manage(Mutex::new(grpc_handle));
// Add plugin manager
let grpc_handle = GrpcHandle::new(&app.app_handle());
app.manage(Mutex::new(grpc_handle));
let app_handle = app.app_handle().clone();
monitor_plugin_events(&app_handle);
Ok(())
})
@@ -1715,10 +1697,9 @@ pub fn run() {
.run(|app_handle, event| {
match event {
RunEvent::Ready => {
create_window(app_handle, "/");
let h = app_handle.clone();
let w = create_window(app_handle, "/");
tauri::async_runtime::spawn(async move {
let info = analytics::track_launch_event(&h).await;
let info = analytics::track_launch_event(&w).await;
debug!("Launched Yaak {:?}", info);
});
@@ -1743,10 +1724,12 @@ pub fn run() {
let h = app_handle.clone();
tauri::async_runtime::spawn(async move {
let windows = h.webview_windows();
let w = windows.values().next().unwrap();
tokio::time::sleep(Duration::from_millis(4000)).await;
let val: State<'_, Mutex<YaakNotifier>> = h.state();
let val: State<'_, Mutex<YaakNotifier>> = w.state();
let mut n = val.lock().await;
if let Err(e) = n.check(&h).await {
if let Err(e) = n.check(&w).await {
warn!("Failed to check for notifications {}", e)
}
});
@@ -1905,3 +1888,86 @@ fn safe_uri(endpoint: &str) -> String {
format!("http://{}", endpoint)
}
}
fn monitor_plugin_events<R: Runtime>(app_handle: &AppHandle<R>) {
let app_handle = app_handle.clone();
tauri::async_runtime::spawn(async move {
let plugin_manager: State<'_, PluginManager> = app_handle.state();
let (_rx_id, mut rx) = plugin_manager.subscribe().await;
let app_handle = app_handle.clone();
while let Some(event) = rx.recv().await {
let payload = match handle_plugin_event(&app_handle, &event).await {
Some(e) => e,
None => continue,
};
if let Err(e) = plugin_manager.reply(&event, &payload).await {
warn!("Failed to reply to plugin manager: {}", e)
}
}
});
}
async fn handle_plugin_event<R: Runtime>(
app_handle: &AppHandle<R>,
event: &InternalEvent,
) -> Option<InternalEventPayload> {
let event = match event.clone().payload {
InternalEventPayload::GetHttpRequestByIdRequest(req) => {
let http_request = get_http_request(app_handle, req.id.as_str()).await.ok();
InternalEventPayload::GetHttpRequestByIdResponse(GetHttpRequestByIdResponse {
http_request,
})
}
InternalEventPayload::SendHttpRequestRequest(req) => {
let webview_windows = app_handle.get_focused_window()?.webview_windows();
let w = match webview_windows.iter().next() {
None => return None,
Some((_, w)) => w,
};
let url = w.url().unwrap();
let mut query_pairs = url.query_pairs();
let cookie_jar_id = query_pairs
.find(|(k, _v)| k == "cookie_jar_id")
.map(|(_k, v)| v.to_string());
let cookie_jar = match cookie_jar_id {
None => None,
Some(id) => get_cookie_jar(w, id.as_str()).await.ok(),
};
let environment_id = query_pairs
.find(|(k, _v)| k == "environment_id")
.map(|(_k, v)| v.to_string());
let environment = match environment_id {
None => None,
Some(id) => get_environment(w, id.as_str()).await.ok(),
};
let resp = create_default_http_response(w, req.http_request.id.as_str())
.await
.unwrap();
let result = send_http_request(
&w,
req.http_request,
&resp,
environment,
cookie_jar,
&mut watch::channel(false).1, // No-op cancel channel
)
.await;
let http_response = match result {
Ok(r) => r,
Err(_e) => return None,
};
InternalEventPayload::SendHttpRequestResponse(SendHttpRequestResponse { http_response })
}
_ => return None,
};
Some(event)
}

View File

@@ -5,7 +5,7 @@ use chrono::{DateTime, Duration, Utc};
use log::debug;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Emitter};
use tauri::{Emitter, Manager, Runtime, WebviewWindow};
use yaak_models::queries::{get_key_value_raw, set_key_value_raw};
// Check for updates every hour
@@ -42,16 +42,16 @@ impl YaakNotifier {
}
}
pub async fn seen(&mut self, app: &AppHandle, id: &str) -> Result<(), String> {
let mut seen = get_kv(app).await?;
pub async fn seen<R: Runtime>(&mut self, w: &WebviewWindow<R>, id: &str) -> Result<(), String> {
let mut seen = get_kv(w).await?;
seen.push(id.to_string());
debug!("Marked notification as seen {}", id);
let seen_json = serde_json::to_string(&seen).map_err(|e| e.to_string())?;
set_key_value_raw(app, KV_NAMESPACE, KV_KEY, seen_json.as_str()).await;
set_key_value_raw(w, KV_NAMESPACE, KV_KEY, seen_json.as_str()).await;
Ok(())
}
pub async fn check(&mut self, app: &AppHandle) -> Result<(), String> {
pub async fn check<R: Runtime>(&mut self, w: &WebviewWindow<R>) -> Result<(), String> {
let ignore_check = self.last_check.elapsed().unwrap().as_secs() < MAX_UPDATE_CHECK_SECONDS;
if ignore_check {
@@ -60,8 +60,8 @@ impl YaakNotifier {
self.last_check = SystemTime::now();
let num_launches = get_num_launches(app).await;
let info = app.package_info().clone();
let num_launches = get_num_launches(w).await;
let info = w.app_handle().package_info().clone();
let req = reqwest::Client::default()
.request(Method::GET, "https://notify.yaak.app/notifications")
.query(&[
@@ -80,21 +80,21 @@ impl YaakNotifier {
.map_err(|e| e.to_string())?;
let age = notification.timestamp.signed_duration_since(Utc::now());
let seen = get_kv(app).await?;
let seen = get_kv(w).await?;
if seen.contains(&notification.id) || (age > Duration::days(2)) {
debug!("Already seen notification {}", notification.id);
return Ok(());
}
debug!("Got notification {:?}", notification);
let _ = app.emit("notification", notification.clone());
let _ = w.emit("notification", notification.clone());
Ok(())
}
}
async fn get_kv(app: &AppHandle) -> Result<Vec<String>, String> {
match get_key_value_raw(app, "notifications", "seen").await {
async fn get_kv<R: Runtime>(w: &WebviewWindow<R>) -> Result<Vec<String>, String> {
match get_key_value_raw(w, "notifications", "seen").await {
None => Ok(Vec::new()),
Some(v) => serde_json::from_str(&v.value).map_err(|e| e.to_string()),
}

View File

@@ -1,7 +1,7 @@
use std::collections::HashMap;
use serde_json::Value;
use crate::template_fns::timestamp;
use templates::parse_and_render;
use yaak_templates::parse_and_render;
use yaak_models::models::{
Environment, EnvironmentVariable, HttpRequest, HttpRequestHeader, HttpUrlParameter, Workspace,
};

View File

@@ -1,5 +1,5 @@
[package]
name = "grpc"
name = "yaak_grpc"
version = "0.1.0"
edition = "2021"

View File

@@ -15,10 +15,10 @@ use sea_query::Keyword::CurrentTimestamp;
use sea_query::{Cond, Expr, OnConflict, Order, Query, SqliteQueryBuilder};
use sea_query_rusqlite::RusqliteBinder;
use serde::Serialize;
use tauri::{AppHandle, Emitter, Manager, WebviewWindow, Wry};
use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow};
pub async fn set_key_value_string(
mgr: &impl Manager<Wry>,
pub async fn set_key_value_string<R: Runtime>(
mgr: &WebviewWindow<R>,
namespace: &str,
key: &str,
value: &str,
@@ -27,8 +27,8 @@ pub async fn set_key_value_string(
set_key_value_raw(mgr, namespace, key, &encoded.unwrap()).await
}
pub async fn set_key_value_int(
mgr: &impl Manager<Wry>,
pub async fn set_key_value_int<R: Runtime>(
mgr: &WebviewWindow<R>,
namespace: &str,
key: &str,
value: i32,
@@ -37,8 +37,8 @@ pub async fn set_key_value_int(
set_key_value_raw(mgr, namespace, key, &encoded.unwrap()).await
}
pub async fn get_key_value_string(
mgr: &impl Manager<Wry>,
pub async fn get_key_value_string<R: Runtime>(
mgr: &impl Manager<R>,
namespace: &str,
key: &str,
default: &str,
@@ -58,8 +58,8 @@ pub async fn get_key_value_string(
}
}
pub async fn get_key_value_int(
mgr: &impl Manager<Wry>,
pub async fn get_key_value_int<R: Runtime>(
mgr: &impl Manager<R>,
namespace: &str,
key: &str,
default: i32,
@@ -79,15 +79,15 @@ pub async fn get_key_value_int(
}
}
pub async fn set_key_value_raw(
mgr: &impl Manager<Wry>,
pub async fn set_key_value_raw<R: Runtime>(
w: &WebviewWindow<R>,
namespace: &str,
key: &str,
value: &str,
) -> (KeyValue, bool) {
let existing = get_key_value_raw(mgr, namespace, key).await;
let existing = get_key_value_raw(w, namespace, key).await;
let dbm = &*mgr.state::<SqliteConnection>();
let dbm = &*w.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let (sql, params) = Query::insert()
.into_table(KeyValueIden::Table)
@@ -119,11 +119,11 @@ pub async fn set_key_value_raw(
let kv = stmt
.query_row(&*params.as_params(), |row| row.try_into())
.expect("Failed to upsert KeyValue");
(kv, existing.is_none())
(emit_upserted_model(w, kv), existing.is_none())
}
pub async fn get_key_value_raw(
mgr: &impl Manager<Wry>,
pub async fn get_key_value_raw<R: Runtime>(
mgr: &impl Manager<R>,
namespace: &str,
key: &str,
) -> Option<KeyValue> {
@@ -143,7 +143,7 @@ pub async fn get_key_value_raw(
.ok()
}
pub async fn list_workspaces(mgr: &impl Manager<Wry>) -> Result<Vec<Workspace>> {
pub async fn list_workspaces<R: Runtime>(mgr: &impl Manager<R>) -> Result<Vec<Workspace>> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let (sql, params) = Query::select()
@@ -155,7 +155,7 @@ pub async fn list_workspaces(mgr: &impl Manager<Wry>) -> Result<Vec<Workspace>>
Ok(items.map(|v| v.unwrap()).collect())
}
pub async fn get_workspace(mgr: &impl Manager<Wry>, id: &str) -> Result<Workspace> {
pub async fn get_workspace<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Result<Workspace> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let (sql, params) = Query::select()
@@ -167,7 +167,10 @@ pub async fn get_workspace(mgr: &impl Manager<Wry>, id: &str) -> Result<Workspac
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
}
pub async fn upsert_workspace(window: &WebviewWindow, workspace: Workspace) -> Result<Workspace> {
pub async fn upsert_workspace<R: Runtime>(
window: &WebviewWindow<R>,
workspace: Workspace,
) -> Result<Workspace> {
let id = match workspace.id.as_str() {
"" => generate_model_id(ModelType::TypeWorkspace),
_ => workspace.id.to_string(),
@@ -222,7 +225,10 @@ pub async fn upsert_workspace(window: &WebviewWindow, workspace: Workspace) -> R
Ok(emit_upserted_model(window, m))
}
pub async fn delete_workspace(window: &WebviewWindow, id: &str) -> Result<Workspace> {
pub async fn delete_workspace<R: Runtime>(
window: &WebviewWindow<R>,
id: &str,
) -> Result<Workspace> {
let workspace = get_workspace(window, id).await?;
let dbm = &*window.app_handle().state::<SqliteConnection>();
@@ -241,7 +247,7 @@ pub async fn delete_workspace(window: &WebviewWindow, id: &str) -> Result<Worksp
emit_deleted_model(window, workspace)
}
pub async fn get_cookie_jar(mgr: &impl Manager<Wry>, id: &str) -> Result<CookieJar> {
pub async fn get_cookie_jar<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Result<CookieJar> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
@@ -254,8 +260,8 @@ pub async fn get_cookie_jar(mgr: &impl Manager<Wry>, id: &str) -> Result<CookieJ
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
}
pub async fn list_cookie_jars(
mgr: &impl Manager<Wry>,
pub async fn list_cookie_jars<R: Runtime>(
mgr: &impl Manager<R>,
workspace_id: &str,
) -> Result<Vec<CookieJar>> {
let dbm = &*mgr.state::<SqliteConnection>();
@@ -270,7 +276,10 @@ pub async fn list_cookie_jars(
Ok(items.map(|v| v.unwrap()).collect())
}
pub async fn delete_cookie_jar(window: &WebviewWindow, id: &str) -> Result<CookieJar> {
pub async fn delete_cookie_jar<R: Runtime>(
window: &WebviewWindow<R>,
id: &str,
) -> Result<CookieJar> {
let cookie_jar = get_cookie_jar(window, id).await?;
let dbm = &*window.app_handle().state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
@@ -284,13 +293,19 @@ pub async fn delete_cookie_jar(window: &WebviewWindow, id: &str) -> Result<Cooki
emit_deleted_model(window, cookie_jar)
}
pub async fn duplicate_grpc_request(window: &WebviewWindow, id: &str) -> Result<GrpcRequest> {
pub async fn duplicate_grpc_request<R: Runtime>(
window: &WebviewWindow<R>,
id: &str,
) -> Result<GrpcRequest> {
let mut request = get_grpc_request(window, id).await?.clone();
request.id = "".to_string();
upsert_grpc_request(window, &request).await
}
pub async fn delete_grpc_request(window: &WebviewWindow, id: &str) -> Result<GrpcRequest> {
pub async fn delete_grpc_request<R: Runtime>(
window: &WebviewWindow<R>,
id: &str,
) -> Result<GrpcRequest> {
let req = get_grpc_request(window, id).await?;
let dbm = &*window.app_handle().state::<SqliteConnection>();
@@ -304,8 +319,8 @@ pub async fn delete_grpc_request(window: &WebviewWindow, id: &str) -> Result<Grp
emit_deleted_model(window, req)
}
pub async fn upsert_grpc_request(
window: &WebviewWindow,
pub async fn upsert_grpc_request<R: Runtime>(
window: &WebviewWindow<R>,
request: &GrpcRequest,
) -> Result<GrpcRequest> {
let id = match request.id.as_str() {
@@ -346,7 +361,11 @@ pub async fn upsert_grpc_request(
request.service.as_ref().map(|s| s.as_str()).into(),
request.method.as_ref().map(|s| s.as_str()).into(),
request.message.as_str().into(),
request .authentication_type .as_ref() .map(|s| s.as_str()) .into(),
request
.authentication_type
.as_ref()
.map(|s| s.as_str())
.into(),
serde_json::to_string(&request.authentication)?.into(),
serde_json::to_string(&request.metadata)?.into(),
])
@@ -376,7 +395,7 @@ pub async fn upsert_grpc_request(
Ok(emit_upserted_model(window, m))
}
pub async fn get_grpc_request(mgr: &impl Manager<Wry>, id: &str) -> Result<GrpcRequest> {
pub async fn get_grpc_request<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Result<GrpcRequest> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
@@ -389,8 +408,8 @@ pub async fn get_grpc_request(mgr: &impl Manager<Wry>, id: &str) -> Result<GrpcR
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
}
pub async fn list_grpc_requests(
mgr: &impl Manager<Wry>,
pub async fn list_grpc_requests<R: Runtime>(
mgr: &impl Manager<R>,
workspace_id: &str,
) -> Result<Vec<GrpcRequest>> {
let dbm = &*mgr.state::<SqliteConnection>();
@@ -405,8 +424,8 @@ pub async fn list_grpc_requests(
Ok(items.map(|v| v.unwrap()).collect())
}
pub async fn upsert_grpc_connection(
window: &WebviewWindow,
pub async fn upsert_grpc_connection<R: Runtime>(
window: &WebviewWindow<R>,
connection: &GrpcConnection,
) -> Result<GrpcConnection> {
let id = match connection.id.as_str() {
@@ -467,7 +486,10 @@ pub async fn upsert_grpc_connection(
Ok(emit_upserted_model(window, m))
}
pub async fn get_grpc_connection(mgr: &impl Manager<Wry>, id: &str) -> Result<GrpcConnection> {
pub async fn get_grpc_connection<R: Runtime>(
mgr: &impl Manager<R>,
id: &str,
) -> Result<GrpcConnection> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let (sql, params) = Query::select()
@@ -479,8 +501,8 @@ pub async fn get_grpc_connection(mgr: &impl Manager<Wry>, id: &str) -> Result<Gr
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
}
pub async fn list_grpc_connections(
mgr: &impl Manager<Wry>,
pub async fn list_grpc_connections<R: Runtime>(
mgr: &impl Manager<R>,
request_id: &str,
) -> Result<Vec<GrpcConnection>> {
let dbm = &*mgr.state::<SqliteConnection>();
@@ -497,7 +519,10 @@ pub async fn list_grpc_connections(
Ok(items.map(|v| v.unwrap()).collect())
}
pub async fn delete_grpc_connection(window: &WebviewWindow, id: &str) -> Result<GrpcConnection> {
pub async fn delete_grpc_connection<R: Runtime>(
window: &WebviewWindow<R>,
id: &str,
) -> Result<GrpcConnection> {
let resp = get_grpc_connection(window, id).await?;
let dbm = &*window.app_handle().state::<SqliteConnection>();
@@ -512,14 +537,20 @@ pub async fn delete_grpc_connection(window: &WebviewWindow, id: &str) -> Result<
emit_deleted_model(window, resp)
}
pub async fn delete_all_grpc_connections(window: &WebviewWindow, request_id: &str) -> Result<()> {
pub async fn delete_all_grpc_connections<R: Runtime>(
window: &WebviewWindow<R>,
request_id: &str,
) -> Result<()> {
for r in list_grpc_connections(window, request_id).await? {
delete_grpc_connection(window, &r.id).await?;
}
Ok(())
}
pub async fn upsert_grpc_event(window: &WebviewWindow, event: &GrpcEvent) -> Result<GrpcEvent> {
pub async fn upsert_grpc_event<R: Runtime>(
window: &WebviewWindow<R>,
event: &GrpcEvent,
) -> Result<GrpcEvent> {
let id = match event.id.as_str() {
"" => generate_model_id(ModelType::TypeGrpcEvent),
_ => event.id.to_string(),
@@ -575,7 +606,7 @@ pub async fn upsert_grpc_event(window: &WebviewWindow, event: &GrpcEvent) -> Res
Ok(emit_upserted_model(window, m))
}
pub async fn get_grpc_event(mgr: &impl Manager<Wry>, id: &str) -> Result<GrpcEvent> {
pub async fn get_grpc_event<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Result<GrpcEvent> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let (sql, params) = Query::select()
@@ -587,8 +618,8 @@ pub async fn get_grpc_event(mgr: &impl Manager<Wry>, id: &str) -> Result<GrpcEve
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
}
pub async fn list_grpc_events(
mgr: &impl Manager<Wry>,
pub async fn list_grpc_events<R: Runtime>(
mgr: &impl Manager<R>,
connection_id: &str,
) -> Result<Vec<GrpcEvent>> {
let dbm = &*mgr.state::<SqliteConnection>();
@@ -605,8 +636,8 @@ pub async fn list_grpc_events(
Ok(items.map(|v| v.unwrap()).collect())
}
pub async fn upsert_cookie_jar(
window: &WebviewWindow,
pub async fn upsert_cookie_jar<R: Runtime>(
window: &WebviewWindow<R>,
cookie_jar: &CookieJar,
) -> Result<CookieJar> {
let id = match cookie_jar.id.as_str() {
@@ -653,8 +684,8 @@ pub async fn upsert_cookie_jar(
Ok(emit_upserted_model(window, m))
}
pub async fn list_environments(
mgr: &impl Manager<Wry>,
pub async fn list_environments<R: Runtime>(
mgr: &impl Manager<R>,
workspace_id: &str,
) -> Result<Vec<Environment>> {
let dbm = &*mgr.state::<SqliteConnection>();
@@ -671,7 +702,10 @@ pub async fn list_environments(
Ok(items.map(|v| v.unwrap()).collect())
}
pub async fn delete_environment(window: &WebviewWindow, id: &str) -> Result<Environment> {
pub async fn delete_environment<R: Runtime>(
window: &WebviewWindow<R>,
id: &str,
) -> Result<Environment> {
let env = get_environment(window, id).await?;
let dbm = &*window.app_handle().state::<SqliteConnection>();
@@ -686,7 +720,7 @@ pub async fn delete_environment(window: &WebviewWindow, id: &str) -> Result<Envi
emit_deleted_model(window, env)
}
async fn get_settings(mgr: &impl Manager<Wry>) -> Result<Settings> {
async fn get_settings<R: Runtime>(mgr: &impl Manager<R>) -> Result<Settings> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
@@ -699,7 +733,7 @@ async fn get_settings(mgr: &impl Manager<Wry>) -> Result<Settings> {
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
}
pub async fn get_or_create_settings(mgr: &impl Manager<Wry>) -> Settings {
pub async fn get_or_create_settings<R: Runtime>(mgr: &impl Manager<R>) -> Settings {
if let Ok(settings) = get_settings(mgr).await {
return settings;
}
@@ -721,7 +755,10 @@ pub async fn get_or_create_settings(mgr: &impl Manager<Wry>) -> Settings {
.expect("Failed to insert Settings")
}
pub async fn update_settings(window: &WebviewWindow, settings: Settings) -> Result<Settings> {
pub async fn update_settings<R: Runtime>(
window: &WebviewWindow<R>,
settings: Settings,
) -> Result<Settings> {
let dbm = &*window.app_handle().state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
@@ -773,8 +810,8 @@ pub async fn update_settings(window: &WebviewWindow, settings: Settings) -> Resu
Ok(emit_upserted_model(window, m))
}
pub async fn upsert_environment(
window: &WebviewWindow,
pub async fn upsert_environment<R: Runtime>(
window: &WebviewWindow<R>,
environment: Environment,
) -> Result<Environment> {
let id = match environment.id.as_str() {
@@ -821,7 +858,7 @@ pub async fn upsert_environment(
Ok(emit_upserted_model(window, m))
}
pub async fn get_environment(mgr: &impl Manager<Wry>, id: &str) -> Result<Environment> {
pub async fn get_environment<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Result<Environment> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
@@ -834,7 +871,7 @@ pub async fn get_environment(mgr: &impl Manager<Wry>, id: &str) -> Result<Enviro
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
}
pub async fn get_folder(mgr: &impl Manager<Wry>, id: &str) -> Result<Folder> {
pub async fn get_folder<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Result<Folder> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
@@ -847,7 +884,10 @@ pub async fn get_folder(mgr: &impl Manager<Wry>, id: &str) -> Result<Folder> {
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
}
pub async fn list_folders(mgr: &impl Manager<Wry>, workspace_id: &str) -> Result<Vec<Folder>> {
pub async fn list_folders<R: Runtime>(
mgr: &impl Manager<R>,
workspace_id: &str,
) -> Result<Vec<Folder>> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
@@ -862,7 +902,7 @@ pub async fn list_folders(mgr: &impl Manager<Wry>, workspace_id: &str) -> Result
Ok(items.map(|v| v.unwrap()).collect())
}
pub async fn delete_folder(window: &WebviewWindow, id: &str) -> Result<Folder> {
pub async fn delete_folder<R: Runtime>(window: &WebviewWindow<R>, id: &str) -> Result<Folder> {
let folder = get_folder(window, id).await?;
let dbm = &*window.app_handle().state::<SqliteConnection>();
@@ -877,7 +917,7 @@ pub async fn delete_folder(window: &WebviewWindow, id: &str) -> Result<Folder> {
emit_deleted_model(window, folder)
}
pub async fn upsert_folder(window: &WebviewWindow, r: Folder) -> Result<Folder> {
pub async fn upsert_folder<R: Runtime>(window: &WebviewWindow<R>, r: Folder) -> Result<Folder> {
let id = match r.id.as_str() {
"" => generate_model_id(ModelType::TypeFolder),
_ => r.id.to_string(),
@@ -925,13 +965,19 @@ pub async fn upsert_folder(window: &WebviewWindow, r: Folder) -> Result<Folder>
Ok(emit_upserted_model(window, m))
}
pub async fn duplicate_http_request(window: &WebviewWindow, id: &str) -> Result<HttpRequest> {
pub async fn duplicate_http_request<R: Runtime>(
window: &WebviewWindow<R>,
id: &str,
) -> Result<HttpRequest> {
let mut request = get_http_request(window, id).await?.clone();
request.id = "".to_string();
upsert_http_request(window, request).await
}
pub async fn upsert_http_request(window: &WebviewWindow, r: HttpRequest) -> Result<HttpRequest> {
pub async fn upsert_http_request<R: Runtime>(
window: &WebviewWindow<R>,
r: HttpRequest,
) -> Result<HttpRequest> {
let id = match r.id.as_str() {
"" => generate_model_id(ModelType::TypeHttpRequest),
_ => r.id.to_string(),
@@ -1004,8 +1050,8 @@ pub async fn upsert_http_request(window: &WebviewWindow, r: HttpRequest) -> Resu
Ok(emit_upserted_model(window, m))
}
pub async fn list_http_requests(
mgr: &impl Manager<Wry>,
pub async fn list_http_requests<R: Runtime>(
mgr: &impl Manager<R>,
workspace_id: &str,
) -> Result<Vec<HttpRequest>> {
let dbm = &*mgr.state::<SqliteConnection>();
@@ -1021,7 +1067,7 @@ pub async fn list_http_requests(
Ok(items.map(|v| v.unwrap()).collect())
}
pub async fn get_http_request(mgr: &impl Manager<Wry>, id: &str) -> Result<HttpRequest> {
pub async fn get_http_request<R: Runtime>(mgr: &impl Manager<R>, id: &str) -> Result<HttpRequest> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
@@ -1034,7 +1080,10 @@ pub async fn get_http_request(mgr: &impl Manager<Wry>, id: &str) -> Result<HttpR
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
}
pub async fn delete_http_request(window: &WebviewWindow, id: &str) -> Result<HttpRequest> {
pub async fn delete_http_request<R: Runtime>(
window: &WebviewWindow<R>,
id: &str,
) -> Result<HttpRequest> {
let req = get_http_request(window, id).await?;
// DB deletes will cascade but this will delete the files
@@ -1051,9 +1100,30 @@ pub async fn delete_http_request(window: &WebviewWindow, id: &str) -> Result<Htt
emit_deleted_model(window, req)
}
pub async fn create_default_http_response<R: Runtime>(
window: &WebviewWindow<R>,
request_id: &str,
) -> Result<HttpResponse> {
create_http_response(
&window,
request_id,
0,
0,
"",
0,
None,
None,
None,
vec![],
None,
None,
)
.await
}
#[allow(clippy::too_many_arguments)]
pub async fn create_http_response(
window: &WebviewWindow,
pub async fn create_http_response<R: Runtime>(
window: &WebviewWindow<R>,
request_id: &str,
elapsed: i64,
elapsed_headers: i64,
@@ -1146,8 +1216,8 @@ pub async fn cancel_pending_responses(app: &AppHandle) -> Result<()> {
Ok(())
}
pub async fn update_response_if_id(
window: &WebviewWindow,
pub async fn update_response_if_id<R: Runtime>(
window: &WebviewWindow<R>,
response: &HttpResponse,
) -> Result<HttpResponse> {
if response.id.is_empty() {
@@ -1157,8 +1227,8 @@ pub async fn update_response_if_id(
}
}
pub async fn update_response(
window: &WebviewWindow,
pub async fn update_response<R: Runtime>(
window: &WebviewWindow<R>,
response: &HttpResponse,
) -> Result<HttpResponse> {
let dbm = &*window.app_handle().state::<SqliteConnection>();
@@ -1211,7 +1281,10 @@ pub async fn update_response(
Ok(emit_upserted_model(window, m))
}
pub async fn get_http_response(mgr: &impl Manager<Wry>, id: &str) -> Result<HttpResponse> {
pub async fn get_http_response<R: Runtime>(
mgr: &impl Manager<R>,
id: &str,
) -> Result<HttpResponse> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let (sql, params) = Query::select()
@@ -1223,7 +1296,10 @@ pub async fn get_http_response(mgr: &impl Manager<Wry>, id: &str) -> Result<Http
Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?)
}
pub async fn delete_http_response(window: &WebviewWindow, id: &str) -> Result<HttpResponse> {
pub async fn delete_http_response<R: Runtime>(
window: &WebviewWindow<R>,
id: &str,
) -> Result<HttpResponse> {
let resp = get_http_response(window, id).await?;
// Delete the body file if it exists
@@ -1244,15 +1320,18 @@ pub async fn delete_http_response(window: &WebviewWindow, id: &str) -> Result<Ht
emit_deleted_model(window, resp)
}
pub async fn delete_all_http_responses(window: &WebviewWindow, request_id: &str) -> Result<()> {
pub async fn delete_all_http_responses<R: Runtime>(
window: &WebviewWindow<R>,
request_id: &str,
) -> Result<()> {
for r in list_responses(window, request_id, None).await? {
delete_http_response(window, &r.id).await?;
}
Ok(())
}
pub async fn list_responses(
mgr: &impl Manager<Wry>,
pub async fn list_responses<R: Runtime>(
mgr: &impl Manager<R>,
request_id: &str,
limit: Option<i64>,
) -> Result<Vec<HttpResponse>> {
@@ -1271,8 +1350,8 @@ pub async fn list_responses(
Ok(items.map(|v| v.unwrap()).collect())
}
pub async fn list_responses_by_workspace_id(
mgr: &impl Manager<Wry>,
pub async fn list_responses_by_workspace_id<R: Runtime>(
mgr: &impl Manager<R>,
workspace_id: &str,
) -> Result<Vec<HttpResponse>> {
let dbm = &*mgr.state::<SqliteConnection>();
@@ -1288,7 +1367,7 @@ pub async fn list_responses_by_workspace_id(
Ok(items.map(|v| v.unwrap()).collect())
}
pub async fn debug_pool(mgr: &impl Manager<Wry>) {
pub async fn debug_pool<R: Runtime>(mgr: &impl Manager<R>) {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await;
debug!("Debug database state: {:?}", db.state());
@@ -1310,7 +1389,7 @@ struct ModelPayload<M: Serialize + Clone> {
pub window_label: String,
}
fn emit_upserted_model<M: Serialize + Clone>(window: &WebviewWindow, model: M) -> M {
fn emit_upserted_model<M: Serialize + Clone, R: Runtime>(window: &WebviewWindow<R>, model: M) -> M {
let payload = ModelPayload {
model: model.clone(),
window_label: window.label().to_string(),
@@ -1320,7 +1399,10 @@ fn emit_upserted_model<M: Serialize + Clone>(window: &WebviewWindow, model: M) -
model
}
fn emit_deleted_model<M: Serialize + Clone>(window: &WebviewWindow, model: M) -> Result<M> {
fn emit_deleted_model<M: Serialize + Clone, R: Runtime>(
window: &WebviewWindow<R>,
model: M,
) -> Result<M> {
let payload = ModelPayload {
model: model.clone(),
window_label: window.label().to_string(),

View File

@@ -1,7 +1,10 @@
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use yaak_models::models::{CookieJar, Environment, Folder, GrpcConnection, GrpcEvent, GrpcRequest, HttpRequest, HttpResponse, KeyValue, Settings, Workspace};
use yaak_models::models::{
CookieJar, Environment, Folder, GrpcConnection, GrpcEvent, GrpcRequest, HttpRequest,
HttpResponse, KeyValue, Settings, Workspace,
};
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
@@ -20,12 +23,22 @@ pub struct InternalEvent {
pub enum InternalEventPayload {
BootRequest(BootRequest),
BootResponse(BootResponse),
ImportRequest(ImportRequest),
ImportResponse(ImportResponse),
FilterRequest(FilterRequest),
FilterResponse(FilterResponse),
ExportHttpRequestRequest(ExportHttpRequestRequest),
ExportHttpRequestResponse(ExportHttpRequestResponse),
SendHttpRequestRequest(SendHttpRequestRequest),
SendHttpRequestResponse(SendHttpRequestResponse),
GetHttpRequestByIdRequest(GetHttpRequestByIdRequest),
GetHttpRequestByIdResponse(GetHttpRequestByIdResponse),
/// Returned when a plugin doesn't get run, just so the server
/// has something to listen for
EmptyResponse(EmptyResponse),
@@ -95,17 +108,33 @@ pub struct ExportHttpRequestResponse {
pub content: String,
}
// TODO: Migrate plugins to return this type
// #[derive(Debug, Clone, Serialize, Deserialize, TS)]
// #[serde(rename_all = "camelCase", untagged)]
// #[ts(export)]
// pub enum ExportableModel {
// Workspace(Workspace),
// Environment(Environment),
// Folder(Folder),
// HttpRequest(HttpRequest),
// GrpcRequest(GrpcRequest),
// }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct SendHttpRequestRequest {
pub http_request: HttpRequest,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct SendHttpRequestResponse {
pub http_response: HttpResponse,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct GetHttpRequestByIdRequest {
pub id: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct GetHttpRequestByIdResponse {
pub http_request: Option<HttpRequest>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]

View File

@@ -1,7 +1,7 @@
use crate::error::Result;
use crate::events::{
ExportHttpRequestRequest, ExportHttpRequestResponse, FilterRequest, FilterResponse,
ImportRequest, ImportResponse, InternalEventPayload,
ExportHttpRequestRequest, ExportHttpRequestResponse, FilterRequest, FilterResponse
, ImportRequest, ImportResponse, InternalEvent, InternalEventPayload,
};
use crate::error::Error::PluginErr;
@@ -10,6 +10,7 @@ use crate::plugin::start_server;
use crate::server::PluginRuntimeGrpcServer;
use std::time::Duration;
use tauri::{AppHandle, Runtime};
use tokio::sync::mpsc;
use tokio::sync::watch::Sender;
use yaak_models::models::HttpRequest;
@@ -35,14 +36,33 @@ impl PluginManager {
PluginManager { kill_tx, server }
}
pub async fn cleanup(&mut self) {
pub async fn subscribe(&self) -> (String, mpsc::Receiver<InternalEvent>) {
self.server.subscribe().await
}
pub async fn unsubscribe(&self, rx_id: &str) {
self.server.unsubscribe(rx_id).await
}
pub async fn cleanup(&self) {
self.kill_tx.send_replace(true);
// Give it a bit of time to kill
tokio::time::sleep(Duration::from_millis(500)).await;
}
pub async fn run_import(&mut self, content: &str) -> Result<(ImportResponse, String)> {
pub async fn reply(
&self,
source_event: &InternalEvent,
payload: &InternalEventPayload,
) -> Result<()> {
let reply_id = Some(source_event.clone().id);
self.server
.send(&payload, source_event.plugin_ref_id.as_str(), reply_id)
.await
}
pub async fn run_import(&self, content: &str) -> Result<(ImportResponse, String)> {
let reply_events = self
.server
.send_and_wait(&InternalEventPayload::ImportRequest(ImportRequest {
@@ -67,7 +87,7 @@ impl PluginManager {
}
pub async fn run_export_curl(
&mut self,
&self,
request: &HttpRequest,
) -> Result<ExportHttpRequestResponse> {
let event = self
@@ -90,7 +110,7 @@ impl PluginManager {
}
pub async fn run_filter(
&mut self,
&self,
filter: &str,
content: &str,
content_type: &str,

View File

@@ -13,7 +13,6 @@ use tauri::plugin::{Builder, TauriPlugin};
use tauri::{Manager, RunEvent, Runtime, State};
use tokio::fs::read_dir;
use tokio::net::TcpListener;
use tokio::sync::Mutex;
use tonic::codegen::tokio_stream;
use tonic::transport::Server;
@@ -30,8 +29,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
.await
.expect("Failed to read plugins dir");
let manager = PluginManager::new(&app, plugin_dirs).await;
let manager_state = Mutex::new(manager);
app.manage(manager_state);
app.manage(manager);
Ok(())
})
})
@@ -41,8 +39,8 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
api.prevent_exit();
tauri::async_runtime::block_on(async move {
info!("Exiting plugin runtime due to app exit");
let manager: State<Mutex<PluginManager>> = app.state();
manager.lock().await.cleanup().await;
let manager: State<PluginManager> = app.state();
manager.cleanup().await;
exit(0);
});
}
@@ -79,7 +77,7 @@ pub async fn start_server(
_ => {}
};
}
server.unsubscribe(rx_id).await;
server.unsubscribe(rx_id.as_str()).await;
});
};

View File

@@ -95,8 +95,8 @@ impl PluginRuntimeGrpcServer {
(id, rx)
}
pub async fn unsubscribe(&self, rx_id: String) {
self.subscribers.lock().await.remove(rx_id.as_str());
pub async fn unsubscribe(&self, rx_id: &str) {
self.subscribers.lock().await.remove(rx_id);
}
pub async fn remove_plugins(&self, plugin_ids: Vec<String>) {
@@ -214,6 +214,13 @@ impl PluginRuntimeGrpcServer {
let msg = format!("Failed to find plugin for {plugin_name}");
Err(PluginNotFoundErr(msg))
}
pub async fn send(&self, payload: &InternalEventPayload, plugin_ref_id: &str, reply_id: Option<String>)-> Result<()> {
let plugin = self.plugin_by_ref_id(plugin_ref_id).await?;
let event = plugin.build_event_to_send(payload, reply_id);
plugin.send(&event).await
}
pub async fn send_to_plugin(
&self,
@@ -301,7 +308,7 @@ impl PluginRuntimeGrpcServer {
break;
}
}
server.unsubscribe(rx_id).await;
server.unsubscribe(rx_id.as_str()).await;
found_events
})
@@ -321,30 +328,6 @@ impl PluginRuntimeGrpcServer {
Ok(events)
}
pub async fn send(&self, payload: InternalEventPayload) -> Result<Vec<InternalEvent>> {
let mut events: Vec<InternalEvent> = Vec::new();
let plugins = self.plugin_ref_to_plugin.lock().await;
if plugins.is_empty() {
return Err(NoPluginsErr("Send failed because no plugins exist".into()));
}
for ph in plugins.values() {
let event = ph.build_event_to_send(&payload, None);
self.send_to_plugin_handle(ph, &event).await?;
events.push(event);
}
Ok(events)
}
async fn send_to_plugin_handle(
&self,
plugin: &PluginHandle,
event: &InternalEvent,
) -> Result<()> {
plugin.send(event).await
}
async fn load_plugins(
&self,
to_plugin_tx: mpsc::Sender<tonic::Result<EventStreamEvent>>,

View File

@@ -1,5 +1,5 @@
[package]
name = "templates"
name = "yaak_templates"
version = "0.1.0"
edition = "2021"

View File

@@ -4,9 +4,8 @@ import type { KeyboardEvent, ReactNode } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useActiveCookieJar } from '../hooks/useActiveCookieJar';
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useAppRoutes } from '../hooks/useAppRoutes';
import { useCreateEnvironment } from '../hooks/useCreateEnvironment';
import { useCreateGrpcRequest } from '../hooks/useCreateGrpcRequest';
@@ -56,8 +55,8 @@ const MAX_PER_GROUP = 8;
export function CommandPalette({ onClose }: { onClose: () => void }) {
const [command, setCommand] = useDebouncedState<string>('', 150);
const [selectedItemKey, setSelectedItemKey] = useState<string | null>(null);
const [activeEnvironment, setActiveEnvironmentId] = useActiveEnvironment();
const routes = useAppRoutes();
const activeEnvironmentId = useActiveEnvironmentId();
const workspaces = useWorkspaces();
const environments = useEnvironments();
const recentEnvironments = useRecentEnvironments();
@@ -68,12 +67,11 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
const openWorkspace = useOpenWorkspace();
const createWorkspace = useCreateWorkspace();
const createHttpRequest = useCreateHttpRequest();
const { activeCookieJar } = useActiveCookieJar();
const [activeCookieJar] = useActiveCookieJar();
const createGrpcRequest = useCreateGrpcRequest();
const createEnvironment = useCreateEnvironment();
const dialog = useDialog();
const workspaceId = useActiveWorkspaceId();
const activeEnvironment = useActiveEnvironment();
const workspace = useActiveWorkspace();
const sendRequest = useSendAnyHttpRequest();
const renameRequest = useRenameRequest(activeRequest?.id ?? null);
const deleteRequest = useDeleteRequest(activeRequest?.id ?? null);
@@ -86,9 +84,9 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
label: 'Open Settings',
action: 'settings.show',
onSelect: async () => {
if (workspaceId == null) return;
if (workspace == null) return;
await invokeCmd('cmd_new_nested_window', {
url: routes.paths.workspaceSettings({ workspaceId }),
url: routes.paths.workspaceSettings({ workspaceId: workspace.id }),
label: 'settings',
title: 'Yaak Settings',
});
@@ -190,7 +188,7 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
routes.paths,
sendRequest,
setSidebarHidden,
workspaceId,
workspace,
]);
const sortedRequests = useMemo(() => {
@@ -271,7 +269,7 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
return routes.navigate('request', {
workspaceId: r.workspaceId,
requestId: r.id,
environmentId: activeEnvironmentId ?? undefined,
environmentId: activeEnvironment?.id,
});
},
});
@@ -290,7 +288,7 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
environmentGroup.items.push({
key: `switch-environment-${e.id}`,
label: e.name,
onSelect: () => routes.setEnvironment(e),
onSelect: () => setActiveEnvironmentId(e.id),
});
}
@@ -313,9 +311,9 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
workspaceCommands,
sortedRequests,
routes,
activeEnvironmentId,
activeEnvironment,
sortedEnvironments,
activeEnvironment?.id,
setActiveEnvironmentId,
sortedWorkspaces,
openWorkspace,
]);

View File

@@ -12,7 +12,7 @@ interface Props {
export const CookieDialog = function ({ cookieJarId }: Props) {
const updateCookieJar = useUpdateCookieJar(cookieJarId ?? null);
const cookieJars = useCookieJars();
const cookieJars = useCookieJars().data ?? [];
const cookieJar = cookieJars.find((c) => c.id === cookieJarId);
if (cookieJar == null) {

View File

@@ -12,8 +12,8 @@ import { InlineCode } from './core/InlineCode';
import { useDialog } from './DialogContext';
export function CookieDropdown() {
const cookieJars = useCookieJars();
const { activeCookieJar, setActiveCookieJarId } = useActiveCookieJar();
const cookieJars = useCookieJars().data ?? [];
const [activeCookieJar, setActiveCookieJarId] = useActiveCookieJar();
const updateCookieJar = useUpdateCookieJar(activeCookieJar?.id ?? null);
const deleteCookieJar = useDeleteCookieJar(activeCookieJar ?? null);
const createCookieJar = useCreateCookieJar();

View File

@@ -1,7 +1,6 @@
import classNames from 'classnames';
import { memo, useCallback, useMemo } from 'react';
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
import { useAppRoutes } from '../hooks/useAppRoutes';
import { useEnvironments } from '../hooks/useEnvironments';
import type { ButtonProps } from './core/Button';
import { Button } from './core/Button';
@@ -20,9 +19,8 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
...buttonProps
}: Props) {
const environments = useEnvironments();
const activeEnvironment = useActiveEnvironment();
const [activeEnvironment, setActiveEnvironmentId] = useActiveEnvironment();
const dialog = useDialog();
const routes = useAppRoutes();
const showEnvironmentDialog = useCallback(() => {
dialog.toggle({
@@ -43,9 +41,9 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
leftSlot: e.id === activeEnvironment?.id ? <Icon icon="check" /> : <Icon icon="empty" />,
onSelect: async () => {
if (e.id !== activeEnvironment?.id) {
routes.setEnvironment(e);
setActiveEnvironmentId(e.id);
} else {
routes.setEnvironment(null);
setActiveEnvironmentId(null);
}
},
}),
@@ -62,7 +60,7 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
onSelect: showEnvironmentDialog,
},
],
[activeEnvironment?.id, environments, routes, showEnvironmentDialog],
[activeEnvironment?.id, environments, setActiveEnvironmentId, showEnvironmentDialog],
);
return (

View File

@@ -2,7 +2,8 @@ import { useQueryClient } from '@tanstack/react-query';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import type { Model } from '@yaakapp/api';
import { useEffect } from 'react';
import { useAtiveWorkspaceChangedToast } from '../hooks/useAtiveWorkspaceChangedToast';
import { useEnsureActiveCookieJar, useMigrateActiveCookieJarId } from '../hooks/useActiveCookieJar';
import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast';
import { cookieJarsQueryKey } from '../hooks/useCookieJars';
import { useCopy } from '../hooks/useCopy';
import { environmentsQueryKey } from '../hooks/useEnvironments';
@@ -16,6 +17,7 @@ import { httpResponsesQueryKey } from '../hooks/useHttpResponses';
import { keyValueQueryKey } from '../hooks/useKeyValue';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { useNotificationToast } from '../hooks/useNotificationToast';
import { useRecentCookieJars } from '../hooks/useRecentCookieJars';
import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
import { useRecentRequests } from '../hooks/useRecentRequests';
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
@@ -25,6 +27,7 @@ import { useSyncThemeToDocument } from '../hooks/useSyncThemeToDocument';
import { useToggleCommandPalette } from '../hooks/useToggleCommandPalette';
import { workspacesQueryKey } from '../hooks/useWorkspaces';
import { useZoom } from '../hooks/useZoom';
import { extractKeyValue } from '../lib/keyValueStore';
import { modelsEq } from '../lib/models';
import { catppuccinMacchiato } from '../lib/theme/themes/catppuccin';
import { githubLight } from '../lib/theme/themes/github';
@@ -38,12 +41,17 @@ export function GlobalHooks() {
// Include here so they always update, even if no component references them
useRecentWorkspaces();
useRecentEnvironments();
useRecentCookieJars();
useRecentRequests();
// Other useful things
useSyncThemeToDocument();
useNotificationToast();
useAtiveWorkspaceChangedToast();
useActiveWorkspaceChangedToast();
useEnsureActiveCookieJar();
// TODO: Remove in future version
useMigrateActiveCookieJarId();
const toggleCommandPalette = useToggleCommandPalette();
useHotKey('command_palette.toggle', toggleCommandPalette);
@@ -96,21 +104,28 @@ export function GlobalHooks() {
model.model,
);
if (shouldIgnoreModel(model)) return;
if (shouldIgnoreModel(model, windowLabel)) return;
queryClient.setQueryData<Model[]>(queryKey, (values = []) => {
const index = values.findIndex((v) => modelsEq(v, model)) ?? -1;
if (index >= 0) {
return [...values.slice(0, index), model, ...values.slice(index + 1)];
} else {
return pushToFront ? [model, ...(values ?? [])] : [...(values ?? []), model];
queryClient.setQueryData(queryKey, (current: unknown) => {
if (model.model === 'key_value') {
// Special-case for KeyValue
return extractKeyValue(model);
}
if (Array.isArray(current)) {
const index = current.findIndex((v) => modelsEq(v, model)) ?? -1;
if (index >= 0) {
return [...current.slice(0, index), model, ...current.slice(index + 1)];
} else {
return pushToFront ? [model, ...(current ?? [])] : [...(current ?? []), model];
}
}
});
});
useListenToTauriEvent<ModelPayload>('deleted_model', ({ payload }) => {
const { model } = payload;
if (shouldIgnoreModel(model)) return;
const { model, windowLabel } = payload;
if (shouldIgnoreModel(model, windowLabel)) return;
if (model.model === 'workspace') {
queryClient.setQueryData(workspacesQueryKey(), removeById(model));
@@ -181,7 +196,11 @@ function removeById<T extends { id: string }>(model: T) {
return (entries: T[] | undefined) => entries?.filter((e) => e.id !== model.id);
}
const shouldIgnoreModel = (payload: Model) => {
const shouldIgnoreModel = (payload: Model, windowLabel: string) => {
if (windowLabel === getCurrentWebviewWindow().label) {
// Never ignore same-window updates
return false;
}
if (payload.model === 'key_value') {
return payload.namespace === 'no_sync';
}

View File

@@ -52,12 +52,12 @@ export function MoveToWorkspaceDialog({ onDone, request, activeWorkspaceId }: Pr
if (request.model === 'http_request') {
await updateHttpRequest.mutateAsync(args);
queryClient.invalidateQueries({
await queryClient.invalidateQueries({
queryKey: httpRequestsQueryKey({ workspaceId: activeWorkspaceId }),
});
} else if (request.model === 'grpc_request') {
await updateGrpcRequest.mutateAsync(args);
queryClient.invalidateQueries({
await queryClient.invalidateQueries({
queryKey: grpcRequestsQueryKey({ workspaceId: activeWorkspaceId }),
});
}

View File

@@ -3,7 +3,7 @@ import { useMemo, useRef } from 'react';
import { useKeyPressEvent } from 'react-use';
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useAppRoutes } from '../hooks/useAppRoutes';
import { useHotKey } from '../hooks/useHotKey';
import { useRecentRequests } from '../hooks/useRecentRequests';
@@ -18,8 +18,8 @@ import { HttpMethodTag } from './core/HttpMethodTag';
export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'className'>) {
const dropdownRef = useRef<DropdownRef>(null);
const activeRequest = useActiveRequest();
const activeWorkspaceId = useActiveWorkspaceId();
const activeEnvironment = useActiveEnvironment();
const activeWorkspace = useActiveWorkspace();
const [activeEnvironment] = useActiveEnvironment();
const routes = useAppRoutes();
const allRecentRequestIds = useRecentRequests();
const recentRequestIds = useMemo(() => allRecentRequestIds.slice(1), [allRecentRequestIds]);
@@ -42,7 +42,7 @@ export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'classNa
});
const items = useMemo<DropdownItem[]>(() => {
if (activeWorkspaceId === null) return [];
if (activeWorkspace === null) return [];
const recentRequestItems: DropdownItem[] = [];
for (const id of recentRequestIds) {
@@ -58,7 +58,7 @@ export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'classNa
routes.navigate('request', {
requestId: request.id,
environmentId: activeEnvironment?.id,
workspaceId: activeWorkspaceId,
workspaceId: activeWorkspace.id,
});
},
});
@@ -76,7 +76,7 @@ export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'classNa
}
return recentRequestItems.slice(0, 20);
}, [activeWorkspaceId, activeEnvironment?.id, recentRequestIds, requests, routes]);
}, [activeWorkspace, activeEnvironment?.id, recentRequestIds, requests, routes]);
return (
<Dropdown ref={dropdownRef} items={items}>

View File

@@ -1,6 +1,6 @@
import { open } from '@tauri-apps/plugin-shell';
import { useRef } from 'react';
import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useAppInfo } from '../hooks/useAppInfo';
import { useAppRoutes } from '../hooks/useAppRoutes';
import { useCheckForUpdates } from '../hooks/useCheckForUpdates';
@@ -23,12 +23,12 @@ export function SettingsDropdown() {
const dialog = useDialog();
const checkForUpdates = useCheckForUpdates();
const routes = useAppRoutes();
const workspaceId = useActiveWorkspaceId();
const workspace = useActiveWorkspace();
const showSettings = async () => {
if (!workspaceId) return;
if (!workspace) return;
await invokeCmd('cmd_new_nested_window', {
url: routes.paths.workspaceSettings({ workspaceId }),
url: routes.paths.workspaceSettings({ workspaceId: workspace.id }),
label: 'settings',
title: 'Yaak Settings',
});

View File

@@ -1,11 +1,12 @@
import type { Folder, GrpcRequest, HttpRequest, Workspace } from '@yaakapp/api';
import classNames from 'classnames';
import type { ReactNode } from 'react';
import React, { Fragment, useCallback, useMemo, useRef, useState } from 'react';
import type { XYCoord } from 'react-dnd';
import { useDrag, useDrop } from 'react-dnd';
import { useKey, useKeyPressEvent } from 'react-use';
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useAppRoutes } from '../hooks/useAppRoutes';
@@ -32,7 +33,6 @@ import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
import { useWorkspaces } from '../hooks/useWorkspaces';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import type { Folder, GrpcRequest, HttpRequest, Workspace } from '@yaakapp/api';
import { isResponseLoading } from '../lib/models';
import type { DropdownItem } from './core/Dropdown';
import { ContextMenu } from './core/Dropdown';
@@ -61,7 +61,7 @@ export function Sidebar({ className }: Props) {
const [hidden, setHidden] = useSidebarHidden();
const sidebarRef = useRef<HTMLLIElement>(null);
const activeRequest = useActiveRequest();
const activeEnvironmentId = useActiveEnvironmentId();
const [activeEnvironment] = useActiveEnvironment();
const folders = useFolders();
const requests = useRequests();
const activeWorkspace = useActiveWorkspace();
@@ -207,14 +207,14 @@ export function Sidebar({ className }: Props) {
routes.navigate('request', {
requestId: id,
workspaceId: item.workspaceId,
environmentId: activeEnvironmentId ?? undefined,
environmentId: activeEnvironment?.id,
});
setSelectedId(id);
setSelectedTree(tree);
if (!opts.noFocus) focusActiveRequest({ forced: { id, tree } });
}
},
[treeParentMap, collapsed, routes, activeEnvironmentId, focusActiveRequest],
[treeParentMap, collapsed, routes, activeEnvironment, focusActiveRequest],
);
const handleClearSelected = useCallback(() => {
@@ -260,7 +260,7 @@ export function Sidebar({ className }: Props) {
routes.navigate('request', {
requestId: selected.id,
workspaceId: activeWorkspace?.id,
environmentId: activeEnvironmentId ?? undefined,
environmentId: activeEnvironment?.id,
});
});

View File

@@ -5,7 +5,6 @@ import { useCallback, useMemo, useRef, useState } from 'react';
import { useWindowSize } from 'react-use';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId';
import { useFloatingSidebarHidden } from '../hooks/useFloatingSidebarHidden';
import { useImportData } from '../hooks/useImportData';
import { useShouldFloatSidebar } from '../hooks/useShouldFloatSidebar';
@@ -16,7 +15,6 @@ import { useWorkspaces } from '../hooks/useWorkspaces';
import { Banner } from './core/Banner';
import { Button } from './core/Button';
import { HotKeyList } from './core/HotKeyList';
import { InlineCode } from './core/InlineCode';
import { FeedbackLink } from './core/Link';
import { HStack } from './core/Stacks';
import { CreateDropdown } from './CreateDropdown';
@@ -38,7 +36,6 @@ export default function Workspace() {
useSyncWorkspaceRequestTitle();
const workspaces = useWorkspaces();
const activeWorkspace = useActiveWorkspace();
const activeWorkspaceId = useActiveWorkspaceId();
const { setWidth, width, resetWidth } = useSidebarWidth();
const [sidebarHidden, setSidebarHidden] = useSidebarHidden();
const [floatingSidebarHidden, setFloatingSidebarHidden] = useFloatingSidebarHidden();
@@ -176,9 +173,8 @@ export default function Workspace() {
{activeWorkspace == null ? (
<div className="m-auto">
<Banner color="warning" className="max-w-[30rem]">
The active workspace{' '}
<InlineCode className="text-warning">{activeWorkspaceId}</InlineCode> was not found.
Select a workspace from the header menu or report this bug to <FeedbackLink />
The active workspace was not found. Select a workspace from the header menu or report
this bug to <FeedbackLink />
</Banner>
</div>
) : activeRequest == null ? (

View File

@@ -87,7 +87,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
ref,
) {
const s = useSettings();
const e = useActiveEnvironment();
const [e] = useActiveEnvironment();
const w = useActiveWorkspace();
const environment = autocompleteVariables ? e : null;
const workspace = autocompleteVariables ? w : null;

View File

@@ -9,7 +9,6 @@ class PlaceholderWidget extends WidgetType {
readonly exists: boolean,
readonly type: 'function' | 'variable' = 'variable',
) {
console.log('PLACEHOLDER', { name, value });
super();
}

View File

@@ -3,7 +3,7 @@ import classNames from 'classnames';
import type { CSSProperties, MouseEvent as ReactMouseEvent, ReactNode } from 'react';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { useLocalStorage } from 'react-use';
import { useActiveWorkspaceId } from '../../hooks/useActiveWorkspaceId';
import { useActiveWorkspace } from '../../hooks/useActiveWorkspace';
import { clamp } from '../../lib/clamp';
import { ResizeHandle } from '../ResizeHandle';
@@ -42,10 +42,13 @@ export function SplitLayout({
minWidthPx = 10,
}: Props) {
const containerRef = useRef<HTMLDivElement>(null);
const activeWorkspace = useActiveWorkspace();
const [verticalBasedOnSize, setVerticalBasedOnSize] = useState<boolean>(false);
const [widthRaw, setWidth] = useLocalStorage<number>(`${name}_width::${useActiveWorkspaceId()}`);
const [widthRaw, setWidth] = useLocalStorage<number>(
`${name}_width::${activeWorkspace?.id ?? 'n/a'}`,
);
const [heightRaw, setHeight] = useLocalStorage<number>(
`${name}_height::${useActiveWorkspaceId()}`,
`${name}_height::${activeWorkspace?.id ?? 'n/a'}`,
);
const width = widthRaw ?? defaultRatio;
let height = heightRaw ?? defaultRatio;

View File

@@ -1,36 +1,87 @@
import { useEffect, useMemo } from 'react';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useCallback, useEffect, useMemo } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useActiveWorkspace } from './useActiveWorkspace';
import { useCookieJars } from './useCookieJars';
import { useKeyValue } from './useKeyValue';
export const QUERY_COOKIE_JAR_ID = 'cookie_jar_id';
export function useActiveCookieJar() {
const workspaceId = useActiveWorkspaceId();
const [activeCookieJarId, setActiveCookieJarId] = useActiveCookieJarId();
const cookieJars = useCookieJars();
const activeCookieJar = useMemo(() => {
if (cookieJars.data == null) return undefined;
return cookieJars.data.find((cookieJar) => cookieJar.id === activeCookieJarId) ?? null;
}, [activeCookieJarId, cookieJars.data]);
return [activeCookieJar ?? null, setActiveCookieJarId] as const;
}
export function useEnsureActiveCookieJar() {
const cookieJars = useCookieJars();
const [activeCookieJarId, setActiveCookieJarId] = useActiveCookieJarId();
useEffect(() => {
if (cookieJars.data == null) return;
if (cookieJars.data.find((j) => j.id === activeCookieJarId)) {
return; // There's an active jar
}
const firstJar = cookieJars.data[0];
if (firstJar == null) {
console.log("Workspace doesn't have any cookie jars to activate");
return;
}
// There's no active jar, so set it to the first one
console.log('Setting active cookie jar to', firstJar.id);
setActiveCookieJarId(firstJar.id);
}, [activeCookieJarId, cookieJars, cookieJars.data, setActiveCookieJarId]);
}
export function useMigrateActiveCookieJarId() {
const workspace = useActiveWorkspace();
const [, setActiveCookieJarId] = useActiveCookieJarId();
const {
set: setActiveCookieJarId,
value: activeCookieJarId,
isLoading: isLoadingActiveCookieJarId,
set: setLegacyActiveCookieJarId,
value: legacyActiveCookieJarId,
isLoading: isLoadingLegacyActiveCookieJarId,
} = useKeyValue<string | null>({
namespace: 'global',
key: ['activeCookieJar', workspaceId ?? 'n/a'],
key: ['activeCookieJar', workspace?.id ?? 'n/a'],
fallback: null,
});
const activeCookieJar = useMemo(
() => cookieJars.find((cookieJar) => cookieJar.id === activeCookieJarId),
[activeCookieJarId, cookieJars],
useEffect(() => {
if (legacyActiveCookieJarId == null || isLoadingLegacyActiveCookieJarId) return;
console.log('Migrating active cookie jar ID to query param', legacyActiveCookieJarId);
setActiveCookieJarId(legacyActiveCookieJarId);
setLegacyActiveCookieJarId(null).catch(console.error);
}, [
workspace,
isLoadingLegacyActiveCookieJarId,
legacyActiveCookieJarId,
setActiveCookieJarId,
setLegacyActiveCookieJarId,
]);
}
function useActiveCookieJarId() {
// NOTE: This query param is accessed from Rust side, so do not change
const [params, setParams] = useSearchParams();
const id = params.get(QUERY_COOKIE_JAR_ID);
const setId = useCallback(
(id: string) => {
setParams((p) => {
const existing = Object.fromEntries(p);
return { ...existing, [QUERY_COOKIE_JAR_ID]: id };
});
},
[setParams],
);
// TODO: Make this not be called so many times (move to GlobalHooks?)
useEffect(() => {
if (!isLoadingActiveCookieJarId && activeCookieJar == null && cookieJars.length > 0) {
setActiveCookieJarId(cookieJars[0]?.id ?? null).catch(console.error);
}
}, [activeCookieJar, cookieJars, isLoadingActiveCookieJarId, setActiveCookieJarId]);
return {
activeCookieJar: activeCookieJar ?? null,
setActiveCookieJarId: setActiveCookieJarId,
};
return [id, setId] as const;
}

View File

@@ -1,10 +1,38 @@
import { useMemo } from 'react';
import type { Environment } from '@yaakapp/api';
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
import { useCallback, useMemo } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useEnvironments } from './useEnvironments';
export function useActiveEnvironment(): Environment | null {
const id = useActiveEnvironmentId();
export function useActiveEnvironment() {
const [id, setId] = useActiveEnvironmentId();
const environments = useEnvironments();
return useMemo(() => environments.find((w) => w.id === id) ?? null, [environments, id]);
const environment = useMemo(
() => environments.find((w) => w.id === id) ?? null,
[environments, id],
);
return [environment, setId] as const;
}
export const QUERY_ENVIRONMENT_ID = 'environment_id';
export function useActiveEnvironmentId() {
// NOTE: This query param is accessed from Rust side, so do not change
const [params, setParams] = useSearchParams();
const id = params.get(QUERY_ENVIRONMENT_ID);
const setId = useCallback(
(id: string | null) => {
setParams((p) => {
const existing = Object.fromEntries(p);
if (id == null) {
delete existing[QUERY_ENVIRONMENT_ID];
} else {
existing[QUERY_ENVIRONMENT_ID] = id;
}
return existing;
});
},
[setParams],
);
return [id, setId] as const;
}

View File

@@ -1,13 +0,0 @@
import { useSearchParams } from 'react-router-dom';
export const QUERY_ENVIRONMENT_ID = 'environment_id';
export function useActiveEnvironmentId(): string | null {
const [params] = useSearchParams();
const environmentId = params.get(QUERY_ENVIRONMENT_ID);
if (environmentId == null) {
return null;
}
return environmentId;
}

View File

@@ -1,6 +1,7 @@
import { useMemo } from 'react';
import type { Workspace } from '@yaakapp/api';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useParams } from 'react-router-dom';
import type { RouteParamsWorkspace } from './useAppRoutes';
import { useWorkspaces } from './useWorkspaces';
export function useActiveWorkspace(): Workspace | null {
@@ -11,3 +12,8 @@ export function useActiveWorkspace(): Workspace | null {
[workspaces, workspaceId],
);
}
function useActiveWorkspaceId(): string | null {
const { workspaceId } = useParams<RouteParamsWorkspace>();
return workspaceId ?? null;
}

View File

@@ -3,7 +3,7 @@ import { InlineCode } from '../components/core/InlineCode';
import { useToast } from '../components/ToastContext';
import { useActiveWorkspace } from './useActiveWorkspace';
export function useAtiveWorkspaceChangedToast() {
export function useActiveWorkspaceChangedToast() {
const toast = useToast();
const activeWorkspace = useActiveWorkspace();
const [id, setId] = useState<string | null>(activeWorkspace?.id ?? null);

View File

@@ -1,7 +0,0 @@
import { useParams } from 'react-router-dom';
import type { RouteParamsWorkspace } from './useAppRoutes';
export function useActiveWorkspaceId(): string | null {
const { workspaceId } = useParams<RouteParamsWorkspace>();
return workspaceId ?? null;
}

View File

@@ -1,13 +1,15 @@
import type { Environment } from '@yaakapp/api';
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import type { Environment } from '@yaakapp/api';
import { QUERY_ENVIRONMENT_ID } from './useActiveEnvironmentId';
import { QUERY_COOKIE_JAR_ID } from './useActiveCookieJar';
import { QUERY_ENVIRONMENT_ID } from './useActiveEnvironment';
import { useActiveRequestId } from './useActiveRequestId';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useActiveWorkspace } from './useActiveWorkspace';
export type RouteParamsWorkspace = {
workspaceId: string;
environmentId?: string;
cookieJarId?: string;
};
export type RouteParamsRequest = RouteParamsWorkspace & {
@@ -22,34 +24,35 @@ export const routePaths = {
return `/workspaces/${workspaceId}/settings`;
},
workspace(
{ workspaceId, environmentId } = {
{ workspaceId, environmentId, cookieJarId } = {
workspaceId: ':workspaceId',
environmentId: ':environmentId',
cookieJarId: ':cookieJarId',
} as RouteParamsWorkspace,
) {
let path = `/workspaces/${workspaceId}`;
if (environmentId != null) {
path += `?${QUERY_ENVIRONMENT_ID}=${encodeURIComponent(environmentId)}`;
}
return path;
const path = `/workspaces/${workspaceId}`;
const params = new URLSearchParams();
if (environmentId != null) params.set(QUERY_ENVIRONMENT_ID, environmentId);
if (cookieJarId != null) params.set(QUERY_COOKIE_JAR_ID, cookieJarId);
return `${path}?${params}`;
},
request(
{ workspaceId, environmentId, requestId } = {
{ workspaceId, environmentId, requestId, cookieJarId } = {
workspaceId: ':workspaceId',
environmentId: ':environmentId',
requestId: ':requestId',
} as RouteParamsRequest,
) {
let path = `/workspaces/${workspaceId}/requests/${requestId}`;
if (environmentId != null) {
path += `?${QUERY_ENVIRONMENT_ID}=${encodeURIComponent(environmentId)}`;
}
return path;
const path = `/workspaces/${workspaceId}/requests/${requestId}`;
const params = new URLSearchParams();
if (environmentId != null) params.set(QUERY_ENVIRONMENT_ID, environmentId);
if (cookieJarId != null) params.set(QUERY_COOKIE_JAR_ID, cookieJarId);
return `${path}?${params}`;
},
};
export function useAppRoutes() {
const activeWorkspaceId = useActiveWorkspaceId();
const activeWorkspace = useActiveWorkspace();
const requestId = useActiveRequestId();
const nav = useNavigate();
@@ -66,22 +69,22 @@ export function useAppRoutes() {
const setEnvironment = useCallback(
(environment: Environment | null) => {
if (activeWorkspaceId == null) {
if (activeWorkspace == null) {
navigate('workspaces');
} else if (requestId == null) {
navigate('workspace', {
workspaceId: activeWorkspaceId,
workspaceId: activeWorkspace.id,
environmentId: environment == null ? undefined : environment.id,
});
} else {
navigate('request', {
workspaceId: activeWorkspaceId,
workspaceId: activeWorkspace.id,
environmentId: environment == null ? undefined : environment.id,
requestId,
});
}
},
[navigate, activeWorkspaceId, requestId],
[navigate, activeWorkspace, requestId],
);
return {

View File

@@ -1,22 +1,22 @@
import { useQuery } from '@tanstack/react-query';
import type { CookieJar } from '../lib/models';
import { invokeCmd } from '../lib/tauri';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useActiveWorkspace } from './useActiveWorkspace';
export function cookieJarsQueryKey({ workspaceId }: { workspaceId: string }) {
return ['cookie_jars', { workspaceId }];
}
export function useCookieJars() {
const workspaceId = useActiveWorkspaceId();
return (
useQuery({
enabled: workspaceId != null,
queryKey: cookieJarsQueryKey({ workspaceId: workspaceId ?? 'n/a' }),
queryFn: async () => {
if (workspaceId == null) return [];
return (await invokeCmd('cmd_list_cookie_jars', { workspaceId })) as CookieJar[];
},
}).data ?? []
);
const workspace = useActiveWorkspace();
return useQuery({
enabled: workspace != null,
queryKey: cookieJarsQueryKey({ workspaceId: workspace?.id ?? 'n/a' }),
queryFn: async () => {
if (workspace == null) return [];
return (await invokeCmd('cmd_list_cookie_jars', {
workspaceId: workspace.id,
})) as CookieJar[];
},
});
}

View File

@@ -1,15 +1,18 @@
import { useMutation } from '@tanstack/react-query';
import { invokeCmd } from '../lib/tauri';
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
import { useActiveEnvironment } from './useActiveEnvironment';
import { useCopy } from './useCopy';
export function useCopyAsCurl(requestId: string) {
const copy = useCopy();
const environmentId = useActiveEnvironmentId();
const [environment] = useActiveEnvironment();
return useMutation({
mutationKey: ['copy_as_curl', requestId],
mutationFn: async () => {
const cmd: string = await invokeCmd('cmd_request_to_curl', { requestId, environmentId });
const cmd: string = await invokeCmd('cmd_request_to_curl', {
requestId,
environmentId: environment?.id,
});
copy(cmd);
return cmd;
},

View File

@@ -1,20 +1,18 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useMutation } from '@tanstack/react-query';
import { trackEvent } from '../lib/analytics';
import type { CookieJar } from '../lib/models';
import { invokeCmd } from '../lib/tauri';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { cookieJarsQueryKey } from './useCookieJars';
import { useActiveWorkspace } from './useActiveWorkspace';
import { usePrompt } from './usePrompt';
export function useCreateCookieJar() {
const workspaceId = useActiveWorkspaceId();
const queryClient = useQueryClient();
const workspace = useActiveWorkspace();
const prompt = usePrompt();
return useMutation<CookieJar>({
mutationKey: ['create_cookie_jar'],
mutationFn: async () => {
if (workspaceId === null) {
if (workspace === null) {
throw new Error("Cannot create cookie jar when there's no active workspace");
}
const name = await prompt({
@@ -25,14 +23,8 @@ export function useCreateCookieJar() {
label: 'Name',
defaultValue: 'My Jar',
});
return invokeCmd('cmd_create_cookie_jar', { workspaceId, name });
return invokeCmd('cmd_create_cookie_jar', { workspaceId: workspace.id, name });
},
onSettled: () => trackEvent('cookie_jar', 'create'),
onSuccess: async (cookieJar) => {
queryClient.setQueryData<CookieJar[]>(
cookieJarsQueryKey({ workspaceId: cookieJar.workspaceId }),
(items) => [...(items ?? []), cookieJar],
);
},
});
}

View File

@@ -1,17 +1,15 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { trackEvent } from '../lib/analytics';
import { useMutation } from '@tanstack/react-query';
import type { Environment } from '@yaakapp/api';
import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useAppRoutes } from './useAppRoutes';
import { environmentsQueryKey } from './useEnvironments';
import { useActiveEnvironment } from './useActiveEnvironment';
import { useActiveWorkspace } from './useActiveWorkspace';
import { usePrompt } from './usePrompt';
export function useCreateEnvironment() {
const routes = useAppRoutes();
const [, setActiveEnvironmentId] = useActiveEnvironment();
const prompt = usePrompt();
const workspaceId = useActiveWorkspaceId();
const queryClient = useQueryClient();
const workspace = useActiveWorkspace();
return useMutation<Environment, unknown, void>({
mutationKey: ['create_environment'],
@@ -25,16 +23,16 @@ export function useCreateEnvironment() {
placeholder: 'My Environment',
defaultValue: 'My Environment',
});
return invokeCmd('cmd_create_environment', { name, variables: [], workspaceId });
return invokeCmd('cmd_create_environment', {
name,
variables: [],
workspaceId: workspace?.id,
});
},
onSettled: () => trackEvent('environment', 'create'),
onSuccess: async (environment) => {
if (workspaceId == null) return;
routes.setEnvironment(environment);
queryClient.setQueryData<Environment[]>(
environmentsQueryKey({ workspaceId }),
(environments) => [...(environments ?? []), environment],
);
if (workspace == null) return;
setActiveEnvironmentId(environment.id);
},
});
}

View File

@@ -1,22 +1,20 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { trackEvent } from '../lib/analytics';
import { useMutation } from '@tanstack/react-query';
import type { Folder } from '@yaakapp/api';
import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri';
import { useActiveRequest } from './useActiveRequest';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { foldersQueryKey } from './useFolders';
import { useActiveWorkspace } from './useActiveWorkspace';
import { usePrompt } from './usePrompt';
export function useCreateFolder() {
const workspaceId = useActiveWorkspaceId();
const workspace = useActiveWorkspace();
const activeRequest = useActiveRequest();
const queryClient = useQueryClient();
const prompt = usePrompt();
return useMutation<Folder, unknown, Partial<Pick<Folder, 'name' | 'sortPriority' | 'folderId'>>>({
mutationKey: ['create_folder'],
mutationFn: async (patch) => {
if (workspaceId === null) {
if (workspace === null) {
throw new Error("Cannot create folder when there's no active workspace");
}
patch.name =
@@ -32,13 +30,8 @@ export function useCreateFolder() {
}));
patch.sortPriority = patch.sortPriority || -Date.now();
patch.folderId = patch.folderId || activeRequest?.folderId;
return invokeCmd('cmd_create_folder', { workspaceId, ...patch });
return invokeCmd('cmd_create_folder', { workspaceId: workspace.id, ...patch });
},
onSettled: () => trackEvent('folder', 'create'),
onSuccess: async (request) => {
await queryClient.invalidateQueries({
queryKey: foldersQueryKey({ workspaceId: request.workspaceId }),
});
},
});
}

View File

@@ -1,15 +1,15 @@
import { useMutation } from '@tanstack/react-query';
import { trackEvent } from '../lib/analytics';
import type { GrpcRequest } from '@yaakapp/api';
import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri';
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
import { useActiveEnvironment } from './useActiveEnvironment';
import { useActiveRequest } from './useActiveRequest';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useActiveWorkspace } from './useActiveWorkspace';
import { useAppRoutes } from './useAppRoutes';
export function useCreateGrpcRequest() {
const workspaceId = useActiveWorkspaceId();
const activeEnvironmentId = useActiveEnvironmentId();
const workspace = useActiveWorkspace();
const [activeEnvironment] = useActiveEnvironment();
const activeRequest = useActiveRequest();
const routes = useAppRoutes();
@@ -20,12 +20,12 @@ export function useCreateGrpcRequest() {
>({
mutationKey: ['create_grpc_request'],
mutationFn: (patch) => {
if (workspaceId === null) {
if (workspace === null) {
throw new Error("Cannot create grpc request when there's no active workspace");
}
if (patch.sortPriority === undefined) {
if (activeRequest != null) {
// Place above currently-active request
// Place above currently active request
patch.sortPriority = activeRequest.sortPriority + 0.0001;
} else {
// Place at the very top
@@ -33,14 +33,18 @@ export function useCreateGrpcRequest() {
}
}
patch.folderId = patch.folderId || activeRequest?.folderId;
return invokeCmd('cmd_create_grpc_request', { workspaceId, name: '', ...patch });
return invokeCmd('cmd_create_grpc_request', {
workspaceId: workspace.id,
name: '',
...patch,
});
},
onSettled: () => trackEvent('grpc_request', 'create'),
onSuccess: async (request) => {
routes.navigate('request', {
workspaceId: request.workspaceId,
requestId: request.id,
environmentId: activeEnvironmentId ?? undefined,
environmentId: activeEnvironment?.id,
});
},
});

View File

@@ -1,22 +1,22 @@
import { useMutation } from '@tanstack/react-query';
import { trackEvent } from '../lib/analytics';
import type { HttpRequest } from '@yaakapp/api';
import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri';
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
import { useActiveEnvironment } from './useActiveEnvironment';
import { useActiveRequest } from './useActiveRequest';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useActiveWorkspace } from './useActiveWorkspace';
import { useAppRoutes } from './useAppRoutes';
export function useCreateHttpRequest() {
const workspaceId = useActiveWorkspaceId();
const activeEnvironmentId = useActiveEnvironmentId();
const workspace = useActiveWorkspace();
const [activeEnvironment] = useActiveEnvironment();
const activeRequest = useActiveRequest();
const routes = useAppRoutes();
return useMutation<HttpRequest, unknown, Partial<HttpRequest>>({
mutationKey: ['create_http_request'],
mutationFn: (patch = {}) => {
if (workspaceId === null) {
if (workspace === null) {
throw new Error("Cannot create request when there's no active workspace");
}
if (patch.sortPriority === undefined) {
@@ -29,14 +29,16 @@ export function useCreateHttpRequest() {
}
}
patch.folderId = patch.folderId || activeRequest?.folderId;
return invokeCmd('cmd_create_http_request', { request: { workspaceId, ...patch } });
return invokeCmd('cmd_create_http_request', {
request: { workspaceId: workspace.id, ...patch },
});
},
onSettled: () => trackEvent('http_request', 'create'),
onSuccess: async (request) => {
routes.navigate('request', {
workspaceId: request.workspaceId,
requestId: request.id,
environmentId: activeEnvironmentId ?? undefined,
environmentId: activeEnvironment?.id,
});
},
});

View File

@@ -1,15 +1,13 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useMutation } from '@tanstack/react-query';
import type { GrpcRequest } from '@yaakapp/api';
import { InlineCode } from '../components/core/InlineCode';
import { trackEvent } from '../lib/analytics';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import type { GrpcRequest } from '@yaakapp/api';
import { getGrpcRequest } from '../lib/store';
import { invokeCmd } from '../lib/tauri';
import { useConfirm } from './useConfirm';
import { grpcRequestsQueryKey } from './useGrpcRequests';
export function useDeleteAnyGrpcRequest() {
const queryClient = useQueryClient();
const confirm = useConfirm();
return useMutation<GrpcRequest | null, string, string>({
@@ -32,13 +30,5 @@ export function useDeleteAnyGrpcRequest() {
return invokeCmd('cmd_delete_grpc_request', { requestId: id });
},
onSettled: () => trackEvent('grpc_request', 'delete'),
onSuccess: async (request) => {
if (request === null) return;
const { workspaceId, id: requestId } = request;
queryClient.setQueryData<GrpcRequest[]>(grpcRequestsQueryKey({ workspaceId }), (requests) =>
(requests ?? []).filter((r) => r.id !== requestId),
);
},
});
}

View File

@@ -1,16 +1,13 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useMutation } from '@tanstack/react-query';
import type { HttpRequest } from '@yaakapp/api';
import { InlineCode } from '../components/core/InlineCode';
import { trackEvent } from '../lib/analytics';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import type { HttpRequest } from '@yaakapp/api';
import { getHttpRequest } from '../lib/store';
import { invokeCmd } from '../lib/tauri';
import { useConfirm } from './useConfirm';
import { httpRequestsQueryKey } from './useHttpRequests';
import { httpResponsesQueryKey } from './useHttpResponses';
export function useDeleteAnyHttpRequest() {
const queryClient = useQueryClient();
const confirm = useConfirm();
return useMutation<HttpRequest | null, string, string>({
@@ -33,15 +30,5 @@ export function useDeleteAnyHttpRequest() {
return invokeCmd('cmd_delete_http_request', { requestId: id });
},
onSettled: () => trackEvent('http_request', 'delete'),
onSuccess: async (request) => {
// Was it cancelled?
if (request === null) return;
const { workspaceId, id: requestId } = request;
queryClient.setQueryData(httpResponsesQueryKey({ requestId }), []); // Responses were deleted
queryClient.setQueryData<HttpRequest[]>(httpRequestsQueryKey({ workspaceId }), (requests) =>
(requests ?? []).filter((r) => r.id !== requestId),
);
},
});
}

View File

@@ -1,13 +1,11 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useMutation } from '@tanstack/react-query';
import { InlineCode } from '../components/core/InlineCode';
import { trackEvent } from '../lib/analytics';
import type { CookieJar } from '../lib/models';
import { invokeCmd } from '../lib/tauri';
import { useConfirm } from './useConfirm';
import { cookieJarsQueryKey } from './useCookieJars';
export function useDeleteCookieJar(cookieJar: CookieJar | null) {
const queryClient = useQueryClient();
const confirm = useConfirm();
return useMutation<CookieJar | null, string>({
@@ -27,13 +25,5 @@ export function useDeleteCookieJar(cookieJar: CookieJar | null) {
return invokeCmd('cmd_delete_cookie_jar', { cookieJarId: cookieJar?.id });
},
onSettled: () => trackEvent('cookie_jar', 'delete'),
onSuccess: async (cookieJar) => {
if (cookieJar === null) return;
const { id: cookieJarId, workspaceId } = cookieJar;
queryClient.setQueryData<CookieJar[]>(cookieJarsQueryKey({ workspaceId }), (cookieJars) =>
cookieJars?.filter((e) => e.id !== cookieJarId),
);
},
});
}

View File

@@ -1,13 +1,11 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useMutation } from '@tanstack/react-query';
import type { Environment } from '@yaakapp/api';
import { InlineCode } from '../components/core/InlineCode';
import { trackEvent } from '../lib/analytics';
import type { Environment, Workspace } from '@yaakapp/api';
import { invokeCmd } from '../lib/tauri';
import { useConfirm } from './useConfirm';
import { environmentsQueryKey } from './useEnvironments';
export function useDeleteEnvironment(environment: Environment | null) {
const queryClient = useQueryClient();
const confirm = useConfirm();
return useMutation<Environment | null, string>({
@@ -27,13 +25,5 @@ export function useDeleteEnvironment(environment: Environment | null) {
return invokeCmd('cmd_delete_environment', { environmentId: environment?.id });
},
onSettled: () => trackEvent('environment', 'delete'),
onSuccess: async (environment) => {
if (environment === null) return;
const { id: environmentId, workspaceId } = environment;
queryClient.setQueryData<Workspace[]>(environmentsQueryKey({ workspaceId }), (environments) =>
environments?.filter((e) => e.id !== environmentId),
);
},
});
}

View File

@@ -1,15 +1,12 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useMutation } from '@tanstack/react-query';
import type { Folder } from '@yaakapp/api';
import { InlineCode } from '../components/core/InlineCode';
import { trackEvent } from '../lib/analytics';
import type { Folder } from '@yaakapp/api';
import { getFolder } from '../lib/store';
import { invokeCmd } from '../lib/tauri';
import { useConfirm } from './useConfirm';
import { foldersQueryKey } from './useFolders';
import { httpRequestsQueryKey } from './useHttpRequests';
export function useDeleteFolder(id: string | null) {
const queryClient = useQueryClient();
const confirm = useConfirm();
return useMutation<Folder | null, string>({
@@ -30,15 +27,5 @@ export function useDeleteFolder(id: string | null) {
return invokeCmd('cmd_delete_folder', { folderId: id });
},
onSettled: () => trackEvent('folder', 'delete'),
onSuccess: async (folder) => {
// Was it cancelled?
if (folder === null) return;
const { workspaceId } = folder;
// Nesting makes it hard to clean things up, so just clear everything that could have been deleted
await queryClient.invalidateQueries({ queryKey: httpRequestsQueryKey({ workspaceId }) });
await queryClient.invalidateQueries({ queryKey: foldersQueryKey({ workspaceId }) });
},
});
}

View File

@@ -1,22 +1,14 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { trackEvent } from '../lib/analytics';
import { useMutation } from '@tanstack/react-query';
import type { GrpcConnection } from '@yaakapp/api';
import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri';
import { grpcConnectionsQueryKey } from './useGrpcConnections';
export function useDeleteGrpcConnection(id: string | null) {
const queryClient = useQueryClient();
return useMutation<GrpcConnection>({
mutationKey: ['delete_grpc_connection', id],
mutationFn: async () => {
return await invokeCmd('cmd_delete_grpc_connection', { id: id });
},
onSettled: () => trackEvent('grpc_connection', 'delete'),
onSuccess: ({ requestId, id: connectionId }) => {
queryClient.setQueryData<GrpcConnection[]>(
grpcConnectionsQueryKey({ requestId }),
(connections) => (connections ?? []).filter((c) => c.id !== connectionId),
);
},
});
}

View File

@@ -1,10 +1,8 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useMutation } from '@tanstack/react-query';
import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri';
import { grpcConnectionsQueryKey } from './useGrpcConnections';
export function useDeleteGrpcConnections(requestId?: string) {
const queryClient = useQueryClient();
return useMutation({
mutationKey: ['delete_grpc_connections', requestId],
mutationFn: async () => {
@@ -12,9 +10,5 @@ export function useDeleteGrpcConnections(requestId?: string) {
await invokeCmd('cmd_delete_all_grpc_connections', { requestId });
},
onSettled: () => trackEvent('grpc_connection', 'delete_many'),
onSuccess: async () => {
if (requestId === undefined) return;
queryClient.setQueryData(grpcConnectionsQueryKey({ requestId }), []);
},
});
}

View File

@@ -1,21 +1,14 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { trackEvent } from '../lib/analytics';
import { useMutation } from '@tanstack/react-query';
import type { HttpResponse } from '@yaakapp/api';
import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri';
import { httpResponsesQueryKey } from './useHttpResponses';
export function useDeleteHttpResponse(id: string | null) {
const queryClient = useQueryClient();
return useMutation<HttpResponse>({
mutationKey: ['delete_http_response', id],
mutationFn: async () => {
return await invokeCmd('cmd_delete_http_response', { id: id });
},
onSettled: () => trackEvent('http_response', 'delete'),
onSuccess: ({ requestId, id: responseId }) => {
queryClient.setQueryData<HttpResponse[]>(httpResponsesQueryKey({ requestId }), (responses) =>
(responses ?? []).filter((response) => response.id !== responseId),
);
},
});
}

View File

@@ -1,10 +1,8 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useMutation } from '@tanstack/react-query';
import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri';
import { httpResponsesQueryKey } from './useHttpResponses';
export function useDeleteHttpResponses(requestId?: string) {
const queryClient = useQueryClient();
return useMutation({
mutationKey: ['delete_http_responses', requestId],
mutationFn: async () => {
@@ -12,9 +10,5 @@ export function useDeleteHttpResponses(requestId?: string) {
await invokeCmd('cmd_delete_all_http_responses', { requestId });
},
onSettled: () => trackEvent('http_response', 'delete_many'),
onSuccess: async () => {
if (requestId === undefined) return;
queryClient.setQueryData(httpResponsesQueryKey({ requestId }), []);
},
});
}

View File

@@ -1,17 +1,14 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useMutation } from '@tanstack/react-query';
import type { Workspace } from '@yaakapp/api';
import { InlineCode } from '../components/core/InlineCode';
import { trackEvent } from '../lib/analytics';
import type { Workspace } from '@yaakapp/api';
import { invokeCmd } from '../lib/tauri';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useActiveWorkspace } from './useActiveWorkspace';
import { useAppRoutes } from './useAppRoutes';
import { useConfirm } from './useConfirm';
import { httpRequestsQueryKey } from './useHttpRequests';
import { workspacesQueryKey } from './useWorkspaces';
export function useDeleteWorkspace(workspace: Workspace | null) {
const queryClient = useQueryClient();
const activeWorkspaceId = useActiveWorkspaceId();
const activeWorkspace = useActiveWorkspace();
const routes = useAppRoutes();
const confirm = useConfirm();
@@ -36,16 +33,9 @@ export function useDeleteWorkspace(workspace: Workspace | null) {
if (workspace === null) return;
const { id: workspaceId } = workspace;
queryClient.setQueryData<Workspace[]>(workspacesQueryKey({}), (workspaces) =>
workspaces?.filter((workspace) => workspace.id !== workspaceId),
);
if (workspaceId === activeWorkspaceId) {
if (workspaceId === activeWorkspace?.id) {
routes.navigate('workspaces');
}
// Also clean up other things that may have been deleted
queryClient.setQueryData(httpRequestsQueryKey({ workspaceId }), []);
await queryClient.invalidateQueries({ queryKey: httpRequestsQueryKey({ workspaceId }) });
},
});
}

View File

@@ -1,10 +1,10 @@
import { useMutation } from '@tanstack/react-query';
import type { GrpcRequest } from '@yaakapp/api';
import { trackEvent } from '../lib/analytics';
import { setKeyValue } from '../lib/keyValueStore';
import type { GrpcRequest } from '@yaakapp/api';
import { invokeCmd } from '../lib/tauri';
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useActiveEnvironment } from './useActiveEnvironment';
import { useActiveWorkspace } from './useActiveWorkspace';
import { useAppRoutes } from './useAppRoutes';
import { protoFilesArgs, useGrpcProtoFiles } from './useGrpcProtoFiles';
@@ -15,8 +15,8 @@ export function useDuplicateGrpcRequest({
id: string | null;
navigateAfter: boolean;
}) {
const activeWorkspaceId = useActiveWorkspaceId();
const activeEnvironmentId = useActiveEnvironmentId();
const activeWorkspace = useActiveWorkspace();
const [activeEnvironment] = useActiveEnvironment();
const routes = useAppRoutes();
const protoFiles = useGrpcProtoFiles(id);
return useMutation<GrpcRequest, string>({
@@ -30,11 +30,11 @@ export function useDuplicateGrpcRequest({
// Also copy proto files to new request
await setKeyValue({ ...protoFilesArgs(request.id), value: protoFiles.value ?? [] });
if (navigateAfter && activeWorkspaceId !== null) {
if (navigateAfter && activeWorkspace !== null) {
routes.navigate('request', {
workspaceId: activeWorkspaceId,
workspaceId: activeWorkspace.id,
requestId: request.id,
environmentId: activeEnvironmentId ?? undefined,
environmentId: activeEnvironment?.id,
});
}
},

View File

@@ -1,9 +1,9 @@
import { useMutation } from '@tanstack/react-query';
import { trackEvent } from '../lib/analytics';
import type { HttpRequest } from '@yaakapp/api';
import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri';
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useActiveEnvironment } from './useActiveEnvironment';
import { useActiveWorkspace } from './useActiveWorkspace';
import { useAppRoutes } from './useAppRoutes';
export function useDuplicateHttpRequest({
@@ -13,8 +13,8 @@ export function useDuplicateHttpRequest({
id: string | null;
navigateAfter: boolean;
}) {
const activeWorkspaceId = useActiveWorkspaceId();
const activeEnvironmentId = useActiveEnvironmentId();
const activeWorkspace = useActiveWorkspace();
const [activeEnvironment] = useActiveEnvironment();
const routes = useAppRoutes();
return useMutation<HttpRequest, string>({
mutationKey: ['duplicate_http_request', id],
@@ -24,11 +24,11 @@ export function useDuplicateHttpRequest({
},
onSettled: () => trackEvent('http_request', 'duplicate'),
onSuccess: async (request) => {
if (navigateAfter && activeWorkspaceId !== null) {
if (navigateAfter && activeWorkspace !== null) {
routes.navigate('request', {
workspaceId: activeWorkspaceId,
workspaceId: activeWorkspace.id,
requestId: request.id,
environmentId: activeEnvironmentId ?? undefined,
environmentId: activeEnvironment?.id,
});
}
},

View File

@@ -1,21 +1,23 @@
import { useQuery } from '@tanstack/react-query';
import type { Environment } from '@yaakapp/api';
import { invokeCmd } from '../lib/tauri';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useActiveWorkspace } from './useActiveWorkspace';
export function environmentsQueryKey({ workspaceId }: { workspaceId: string }) {
return ['environments', { workspaceId }];
}
export function useEnvironments() {
const workspaceId = useActiveWorkspaceId();
const workspace = useActiveWorkspace();
return (
useQuery({
enabled: workspaceId != null,
queryKey: environmentsQueryKey({ workspaceId: workspaceId ?? 'n/a' }),
enabled: workspace != null,
queryKey: environmentsQueryKey({ workspaceId: workspace?.id ?? 'n/a' }),
queryFn: async () => {
if (workspaceId == null) return [];
return (await invokeCmd('cmd_list_environments', { workspaceId })) as Environment[];
if (workspace == null) return [];
return (await invokeCmd('cmd_list_environments', {
workspaceId: workspace.id,
})) as Environment[];
},
}).data ?? []
);

View File

@@ -1,11 +1,11 @@
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useActiveWorkspace } from './useActiveWorkspace';
import { useKeyValue } from './useKeyValue';
export function useFloatingSidebarHidden() {
const activeWorkspaceId = useActiveWorkspaceId();
const activeWorkspace = useActiveWorkspace();
const { set, value } = useKeyValue<boolean>({
namespace: 'no_sync',
key: ['floating_sidebar_hidden', activeWorkspaceId ?? 'n/a'],
key: ['floating_sidebar_hidden', activeWorkspace?.id ?? 'n/a'],
fallback: false,
});

View File

@@ -1,21 +1,21 @@
import { useQuery } from '@tanstack/react-query';
import type { Folder } from '@yaakapp/api';
import { invokeCmd } from '../lib/tauri';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useActiveWorkspace } from './useActiveWorkspace';
export function foldersQueryKey({ workspaceId }: { workspaceId: string }) {
return ['folders', { workspaceId }];
}
export function useFolders() {
const workspaceId = useActiveWorkspaceId();
const workspace = useActiveWorkspace();
return (
useQuery({
enabled: workspaceId != null,
queryKey: foldersQueryKey({ workspaceId: workspaceId ?? 'n/a' }),
enabled: workspace != null,
queryKey: foldersQueryKey({ workspaceId: workspace?.id ?? 'n/a' }),
queryFn: async () => {
if (workspaceId == null) return [];
return (await invokeCmd('cmd_list_folders', { workspaceId })) as Folder[];
if (workspace == null) return [];
return (await invokeCmd('cmd_list_folders', { workspaceId: workspace.id })) as Folder[];
},
}).data ?? []
);

View File

@@ -1,10 +1,10 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import { emit } from '@tauri-apps/api/event';
import type { GrpcConnection, GrpcRequest } from '@yaakapp/api';
import { trackEvent } from '../lib/analytics';
import { minPromiseMillis } from '../lib/minPromiseMillis';
import type { GrpcConnection, GrpcRequest } from '@yaakapp/api';
import { invokeCmd } from '../lib/tauri';
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
import { useActiveEnvironment } from './useActiveEnvironment';
import { useDebouncedValue } from './useDebouncedValue';
export interface ReflectResponseService {
@@ -18,12 +18,12 @@ export function useGrpc(
protoFiles: string[],
) {
const requestId = req?.id ?? 'n/a';
const environmentId = useActiveEnvironmentId();
const [environment] = useActiveEnvironment();
const go = useMutation<void, string>({
mutationKey: ['grpc_go', conn?.id],
mutationFn: async () =>
await invokeCmd('cmd_grpc_go', { requestId, environmentId, protoFiles }),
await invokeCmd('cmd_grpc_go', { requestId, environmentId: environment?.id, protoFiles }),
onSettled: () => trackEvent('grpc_request', 'send'),
});

View File

@@ -1,21 +1,23 @@
import { useQuery } from '@tanstack/react-query';
import type { GrpcRequest } from '@yaakapp/api';
import { invokeCmd } from '../lib/tauri';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useActiveWorkspace } from './useActiveWorkspace';
export function grpcRequestsQueryKey({ workspaceId }: { workspaceId: string }) {
return ['grpc_requests', { workspaceId }];
}
export function useGrpcRequests() {
const workspaceId = useActiveWorkspaceId();
const workspace = useActiveWorkspace();
return (
useQuery({
enabled: workspaceId != null,
queryKey: grpcRequestsQueryKey({ workspaceId: workspaceId ?? 'n/a' }),
enabled: workspace != null,
queryKey: grpcRequestsQueryKey({ workspaceId: workspace?.id ?? 'n/a' }),
queryFn: async () => {
if (workspaceId == null) return [];
return (await invokeCmd('cmd_list_grpc_requests', { workspaceId })) as GrpcRequest[];
if (workspace == null) return [];
return (await invokeCmd('cmd_list_grpc_requests', {
workspaceId: workspace.id,
})) as GrpcRequest[];
},
}).data ?? []
);

View File

@@ -1,21 +1,23 @@
import { useQuery } from '@tanstack/react-query';
import type { HttpRequest } from '@yaakapp/api';
import { invokeCmd } from '../lib/tauri';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useActiveWorkspace } from './useActiveWorkspace';
export function httpRequestsQueryKey({ workspaceId }: { workspaceId: string }) {
return ['http_requests', { workspaceId }];
}
export function useHttpRequests() {
const workspaceId = useActiveWorkspaceId();
const workspace = useActiveWorkspace();
return (
useQuery({
enabled: workspaceId != null,
queryKey: httpRequestsQueryKey({ workspaceId: workspaceId ?? 'n/a' }),
enabled: workspace != null,
queryKey: httpRequestsQueryKey({ workspaceId: workspace?.id ?? 'n/a' }),
queryFn: async () => {
if (workspaceId == null) return [];
return (await invokeCmd('cmd_list_http_requests', { workspaceId })) as HttpRequest[];
if (workspace == null) return [];
return (await invokeCmd('cmd_list_http_requests', {
workspaceId: workspace.id,
})) as HttpRequest[];
},
}).data ?? []
);

View File

@@ -1,13 +1,13 @@
import { useMutation } from '@tanstack/react-query';
import { useToast } from '../components/ToastContext';
import { invokeCmd } from '../lib/tauri';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useActiveWorkspace } from './useActiveWorkspace';
import { useCreateHttpRequest } from './useCreateHttpRequest';
import { useRequestUpdateKey } from './useRequestUpdateKey';
import { useUpdateAnyHttpRequest } from './useUpdateAnyHttpRequest';
export function useImportCurl() {
const workspaceId = useActiveWorkspaceId();
const workspace = useActiveWorkspace();
const updateRequest = useUpdateAnyHttpRequest();
const createRequest = useCreateHttpRequest();
const { wasUpdatedExternally } = useRequestUpdateKey(null);
@@ -24,7 +24,7 @@ export function useImportCurl() {
}) => {
const request: Record<string, unknown> = await invokeCmd('cmd_curl_to_request', {
command,
workspaceId,
workspaceId: workspace?.id,
});
delete request.id;

View File

@@ -1,13 +1,13 @@
import { useMutation } from '@tanstack/react-query';
import type { Environment, Folder, GrpcRequest, HttpRequest, Workspace } from '@yaakapp/api';
import { Button } from '../components/core/Button';
import { FormattedError } from '../components/core/FormattedError';
import { VStack } from '../components/core/Stacks';
import { useDialog } from '../components/DialogContext';
import { ImportDataDialog } from '../components/ImportDataDialog';
import type { Environment, Folder, GrpcRequest, HttpRequest, Workspace } from '@yaakapp/api';
import { count } from '../lib/pluralize';
import { invokeCmd } from '../lib/tauri';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useActiveWorkspace } from './useActiveWorkspace';
import { useAlert } from './useAlert';
import { useAppRoutes } from './useAppRoutes';
@@ -15,7 +15,7 @@ export function useImportData() {
const routes = useAppRoutes();
const dialog = useDialog();
const alert = useAlert();
const activeWorkspaceId = useActiveWorkspaceId();
const activeWorkspace = useActiveWorkspace();
const importData = async (filePath: string): Promise<boolean> => {
const imported: {
@@ -26,7 +26,7 @@ export function useImportData() {
grpcRequests: GrpcRequest[];
} = await invokeCmd('cmd_import_data', {
filePath,
workspaceId: activeWorkspaceId,
workspaceId: activeWorkspace?.id,
});
const importedWorkspace = imported.workspaces[0];

View File

@@ -1,11 +1,11 @@
import type { HttpRequest } from '@yaakapp/api';
import type { IntrospectionQuery } from 'graphql';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { buildClientSchema, getIntrospectionQuery } from '../components/core/Editor';
import { minPromiseMillis } from '../lib/minPromiseMillis';
import type { HttpRequest } from '@yaakapp/api';
import { getResponseBodyText } from '../lib/responseBody';
import { sendEphemeralRequest } from '../lib/sendEphemeralRequest';
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
import { useActiveEnvironment } from './useActiveEnvironment';
import { useDebouncedValue } from './useDebouncedValue';
import { useKeyValue } from './useKeyValue';
@@ -18,7 +18,7 @@ export function useIntrospectGraphQL(baseRequest: HttpRequest) {
// Debounce the request because it can change rapidly and we don't
// want to send so too many requests.
const request = useDebouncedValue(baseRequest);
const activeEnvironmentId = useActiveEnvironmentId();
const [activeEnvironment] = useActiveEnvironment();
const [refetchKey, setRefetchKey] = useState<number>(0);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string>();
@@ -40,7 +40,10 @@ export function useIntrospectGraphQL(baseRequest: HttpRequest) {
bodyType: 'application/json',
body: { text: introspectionRequestBody },
};
const response = await minPromiseMillis(sendEphemeralRequest(args, activeEnvironmentId), 700);
const response = await minPromiseMillis(
sendEphemeralRequest(args, activeEnvironment?.id ?? null),
700,
);
if (response.error) {
throw new Error(response.error);
@@ -72,7 +75,7 @@ export function useIntrospectGraphQL(baseRequest: HttpRequest) {
runIntrospection(); // Run immediately
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [request.id, request.url, request.method, refetchKey, activeEnvironmentId]);
}, [request.id, request.url, request.method, refetchKey, activeEnvironment?.id]);
const refetch = useCallback(() => {
setRefetchKey((k) => k + 1);

View File

@@ -1,4 +1,4 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useMutation, useQuery } from '@tanstack/react-query';
import { useCallback, useMemo } from 'react';
import { buildKeyValueKey, getKeyValue, setKeyValue } from '../lib/keyValueStore';
@@ -24,7 +24,6 @@ export function useKeyValue<T extends Object | null>({
key: string | string[];
fallback: T;
}) {
const queryClient = useQueryClient();
const query = useQuery<T>({
queryKey: keyValueQueryKey({ namespace, key }),
queryFn: async () => getKeyValue({ namespace, key, fallback }),
@@ -34,8 +33,6 @@ export function useKeyValue<T extends Object | null>({
const mutate = useMutation<void, unknown, T>({
mutationKey: ['set_key_value', namespace, key],
mutationFn: (value) => setKeyValue<T>({ namespace, key, value }),
// k/v should be as fast as possible, so optimistically update the cache
onMutate: (value) => queryClient.setQueryData<T>(keyValueQueryKey({ namespace, key }), value),
});
const set = useCallback(

View File

@@ -2,19 +2,19 @@ import { useMutation } from '@tanstack/react-query';
import React from 'react';
import { useDialog } from '../components/DialogContext';
import { MoveToWorkspaceDialog } from '../components/MoveToWorkspaceDialog';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useActiveWorkspace } from './useActiveWorkspace';
import { useRequests } from './useRequests';
export function useMoveToWorkspace(id: string) {
const dialog = useDialog();
const requests = useRequests();
const request = requests.find((r) => r.id === id);
const activeWorkspaceId = useActiveWorkspaceId();
const activeWorkspace = useActiveWorkspace();
return useMutation<void, unknown>({
mutationKey: ['move_workspace', id],
mutationFn: async () => {
if (request == null || activeWorkspaceId == null) return;
if (request == null || activeWorkspace == null) return;
dialog.show({
id: 'change-workspace',
@@ -24,7 +24,7 @@ export function useMoveToWorkspace(id: string) {
<MoveToWorkspaceDialog
onDone={hide}
request={request}
activeWorkspaceId={activeWorkspaceId}
activeWorkspaceId={activeWorkspace.id}
/>
),
});

View File

@@ -1,6 +1,7 @@
import { useMutation } from '@tanstack/react-query';
import { invokeCmd } from '../lib/tauri';
import { useAppRoutes } from './useAppRoutes';
import { getRecentCookieJars } from './useRecentCookieJars';
import { getRecentEnvironments } from './useRecentEnvironments';
import { getRecentRequests } from './useRecentRequests';
@@ -16,29 +17,21 @@ export function useOpenWorkspace() {
workspaceId: string;
inNewWindow: boolean;
}) => {
const environmentId = (await getRecentEnvironments(workspaceId))[0];
const requestId = (await getRecentRequests(workspaceId))[0];
const cookieJarId = (await getRecentCookieJars(workspaceId))[0];
const baseArgs = { workspaceId, environmentId, cookieJarId } as const;
if (inNewWindow) {
const environmentId = (await getRecentEnvironments(workspaceId))[0];
const requestId = (await getRecentRequests(workspaceId))[0];
const path =
requestId != null
? routes.paths.request({
workspaceId,
environmentId,
requestId,
})
: routes.paths.workspace({ workspaceId, environmentId });
? routes.paths.request({ ...baseArgs, requestId })
: routes.paths.workspace({ ...baseArgs });
await invokeCmd('cmd_new_window', { url: path });
} else {
const environmentId = (await getRecentEnvironments(workspaceId))[0];
const requestId = (await getRecentRequests(workspaceId))[0];
if (requestId != null) {
routes.navigate('request', {
workspaceId: workspaceId,
environmentId,
requestId,
});
routes.navigate('request', { ...baseArgs, requestId });
} else {
routes.navigate('workspace', { workspaceId, environmentId });
routes.navigate('workspace', { ...baseArgs });
}
}
},

View File

@@ -1,10 +1,10 @@
import { useEffect, useState } from 'react';
import type { Appearance } from '../lib/theme/appearance';
import {
getCSSAppearance,
getWindowAppearance,
subscribeToWindowAppearanceChange,
} from '../lib/theme/appearance';
import { type Appearance } from '../lib/theme/window';
export function usePreferredAppearance() {
const [preferredAppearance, setPreferredAppearance] = useState<Appearance>(getCSSAppearance());

View File

@@ -0,0 +1,47 @@
import { useEffect, useMemo } from 'react';
import { getKeyValue } from '../lib/keyValueStore';
import { useActiveCookieJar } from './useActiveCookieJar';
import { useActiveWorkspace } from './useActiveWorkspace';
import { useCookieJars } from './useCookieJars';
import { useKeyValue } from './useKeyValue';
const kvKey = (workspaceId: string) => 'recent_cookie_jars::' + workspaceId;
const namespace = 'global';
const fallback: string[] = [];
export function useRecentCookieJars() {
const cookieJars = useCookieJars();
const activeWorkspace = useActiveWorkspace();
const [activeCookieJar] = useActiveCookieJar();
const activeCookieJarId = activeCookieJar?.id ?? null;
const kv = useKeyValue<string[]>({
key: kvKey(activeWorkspace?.id ?? 'n/a'),
namespace,
fallback,
});
// Set history when active request changes
useEffect(() => {
kv.set((currentHistory: string[]) => {
if (activeCookieJarId === null) return currentHistory;
const withoutCurrent = currentHistory.filter((id) => id !== activeCookieJarId);
return [activeCookieJarId, ...withoutCurrent];
}).catch(console.error);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeCookieJarId]);
const onlyValidIds = useMemo(
() => kv.value?.filter((id) => cookieJars.data?.some((e) => e.id === id)) ?? [],
[kv.value, cookieJars],
);
return onlyValidIds;
}
export async function getRecentCookieJars(workspaceId: string) {
return getKeyValue<string[]>({
namespace,
key: kvKey(workspaceId),
fallback,
});
}

View File

@@ -1,7 +1,7 @@
import { useEffect, useMemo } from 'react';
import { getKeyValue } from '../lib/keyValueStore';
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useActiveEnvironment } from './useActiveEnvironment';
import { useActiveWorkspace } from './useActiveWorkspace';
import { useEnvironments } from './useEnvironments';
import { useKeyValue } from './useKeyValue';
@@ -11,10 +11,10 @@ const fallback: string[] = [];
export function useRecentEnvironments() {
const environments = useEnvironments();
const activeWorkspaceId = useActiveWorkspaceId();
const activeEnvironmentId = useActiveEnvironmentId();
const activeWorkspace = useActiveWorkspace();
const [activeEnvironment] = useActiveEnvironment();
const kv = useKeyValue<string[]>({
key: kvKey(activeWorkspaceId ?? 'n/a'),
key: kvKey(activeWorkspace?.id ?? 'n/a'),
namespace,
fallback,
});
@@ -22,12 +22,12 @@ export function useRecentEnvironments() {
// Set history when active request changes
useEffect(() => {
kv.set((currentHistory: string[]) => {
if (activeEnvironmentId === null) return currentHistory;
const withoutCurrentEnvironment = currentHistory.filter((id) => id !== activeEnvironmentId);
return [activeEnvironmentId, ...withoutCurrentEnvironment];
if (activeEnvironment === null) return currentHistory;
const withoutCurrentEnvironment = currentHistory.filter((id) => id !== activeEnvironment.id);
return [activeEnvironment.id, ...withoutCurrentEnvironment];
}).catch(console.error);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeEnvironmentId]);
}, [activeEnvironment?.id]);
const onlyValidIds = useMemo(
() => kv.value?.filter((id) => environments.some((e) => e.id === id)) ?? [],

View File

@@ -1,7 +1,7 @@
import { useEffect, useMemo } from 'react';
import { getKeyValue } from '../lib/keyValueStore';
import { useActiveRequestId } from './useActiveRequestId';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useActiveWorkspace } from './useActiveWorkspace';
import { useKeyValue } from './useKeyValue';
import { useRequests } from './useRequests';
@@ -11,11 +11,11 @@ const fallback: string[] = [];
export function useRecentRequests() {
const requests = useRequests();
const activeWorkspaceId = useActiveWorkspaceId();
const activeWorkspace = useActiveWorkspace();
const activeRequestId = useActiveRequestId();
const kv = useKeyValue<string[]>({
key: kvKey(activeWorkspaceId ?? 'n/a'),
key: kvKey(activeWorkspace?.id ?? 'n/a'),
namespace,
fallback,
});

View File

@@ -1,6 +1,6 @@
import { useEffect, useMemo } from 'react';
import { getKeyValue } from '../lib/keyValueStore';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useActiveWorkspace } from './useActiveWorkspace';
import { useKeyValue } from './useKeyValue';
import { useWorkspaces } from './useWorkspaces';
@@ -10,7 +10,7 @@ const fallback: string[] = [];
export function useRecentWorkspaces() {
const workspaces = useWorkspaces();
const activeWorkspaceId = useActiveWorkspaceId();
const activeWorkspace = useActiveWorkspace();
const kv = useKeyValue<string[]>({
key: kvKey(),
namespace,
@@ -20,12 +20,12 @@ export function useRecentWorkspaces() {
// Set history when active request changes
useEffect(() => {
kv.set((currentHistory: string[]) => {
if (activeWorkspaceId === null) return currentHistory;
const withoutCurrent = currentHistory.filter((id) => id !== activeWorkspaceId);
return [activeWorkspaceId, ...withoutCurrent];
if (activeWorkspace === null) return currentHistory;
const withoutCurrent = currentHistory.filter((id) => id !== activeWorkspace.id);
return [activeWorkspace.id, ...withoutCurrent];
}).catch(console.error);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeWorkspaceId]);
}, [activeWorkspace]);
const onlyValidIds = useMemo(
() => kv.value?.filter((id) => workspaces.some((w) => w.id === id)) ?? [],

View File

@@ -1,18 +1,16 @@
import { useMutation } from '@tanstack/react-query';
import { save } from '@tauri-apps/plugin-dialog';
import slugify from 'slugify';
import { trackEvent } from '../lib/analytics';
import type { HttpResponse } from '@yaakapp/api';
import { trackEvent } from '../lib/analytics';
import { invokeCmd } from '../lib/tauri';
import { useActiveCookieJar } from './useActiveCookieJar';
import { useActiveEnvironment } from './useActiveEnvironment';
import { useAlert } from './useAlert';
import { useHttpRequests } from './useHttpRequests';
export function useSendAnyHttpRequest(options: { download?: boolean } = {}) {
const environment = useActiveEnvironment();
export function useSendAnyHttpRequest() {
const [environment] = useActiveEnvironment();
const alert = useAlert();
const { activeCookieJar } = useActiveCookieJar();
const [activeCookieJar] = useActiveCookieJar();
const requests = useHttpRequests();
return useMutation<HttpResponse | null, string, string | null>({
mutationKey: ['send_any_request'],
@@ -22,21 +20,9 @@ export function useSendAnyHttpRequest(options: { download?: boolean } = {}) {
return null;
}
let downloadDir: string | null = null;
if (options.download) {
downloadDir = await save({
title: 'Select Download Destination',
defaultPath: slugify(request.name, { lower: true, trim: true, strict: true }),
});
if (downloadDir == null) {
return null;
}
}
return invokeCmd('cmd_send_http_request', {
request,
environmentId: environment?.id,
downloadDir: downloadDir,
cookieJarId: activeCookieJar?.id,
});
},

View File

@@ -1,11 +1,11 @@
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useActiveWorkspace } from './useActiveWorkspace';
import { useKeyValue } from './useKeyValue';
export function useSidebarHidden() {
const activeWorkspaceId = useActiveWorkspaceId();
const activeWorkspace = useActiveWorkspace();
const { set, value } = useKeyValue<boolean>({
namespace: 'no_sync',
key: ['sidebar_hidden', activeWorkspaceId ?? 'n/a'],
key: ['sidebar_hidden', activeWorkspace?.id ?? 'n/a'],
fallback: false,
});

View File

@@ -1,10 +1,13 @@
import { useCallback, useMemo } from 'react';
import { useLocalStorage } from 'react-use';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useActiveWorkspace } from './useActiveWorkspace';
export function useSidebarWidth() {
const activeWorkspaceId = useActiveWorkspaceId();
const [width, setWidth] = useLocalStorage<number>(`sidebar_width::${activeWorkspaceId}`, 250);
const activeWorkspace = useActiveWorkspace();
const [width, setWidth] = useLocalStorage<number>(
`sidebar_width::${activeWorkspace?.id ?? 'n/a'}`,
250,
);
const resetWidth = useCallback(() => setWidth(250), [setWidth]);
return useMemo(() => ({ width, setWidth, resetWidth }), [width, setWidth, resetWidth]);
}

View File

@@ -18,6 +18,6 @@ export function useSyncThemeToDocument() {
}
function emitBgChange(t: YaakTheme) {
if (t.background == null) return;
emit('yaak_bg_changed', t.background.hex()).catch(console.error);
if (t.surface == null) return;
emit('yaak_bg_changed', t.surface.hexNoAlpha()).catch(console.error);
}

View File

@@ -11,7 +11,7 @@ import { emit } from '@tauri-apps/api/event';
export function useSyncWorkspaceRequestTitle() {
const activeRequest = useActiveRequest();
const activeWorkspace = useActiveWorkspace();
const activeEnvironment = useActiveEnvironment();
const [activeEnvironment] = useActiveEnvironment();
const osInfo = useOsInfo();
const appInfo = useAppInfo();

View File

@@ -1,12 +1,9 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useMutation } from '@tanstack/react-query';
import type { Folder } from '@yaakapp/api';
import { getFolder } from '../lib/store';
import { invokeCmd } from '../lib/tauri';
import { foldersQueryKey } from './useFolders';
export function useUpdateAnyFolder() {
const queryClient = useQueryClient();
return useMutation<void, unknown, { id: string; update: (r: Folder) => Folder }>({
mutationKey: ['update_any_folder'],
mutationFn: async ({ id, update }) => {
@@ -17,12 +14,5 @@ export function useUpdateAnyFolder() {
await invokeCmd('cmd_update_folder', { folder: update(folder) });
},
onMutate: async ({ id, update }) => {
const folder = await getFolder(id);
if (folder === null) return;
queryClient.setQueryData<Folder[]>(foldersQueryKey(folder), (folders) =>
(folders ?? []).map((f) => (f.id === folder.id ? update(f) : f)),
);
},
});
}

View File

@@ -1,12 +1,9 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useMutation } from '@tanstack/react-query';
import type { GrpcRequest } from '@yaakapp/api';
import { getGrpcRequest } from '../lib/store';
import { invokeCmd } from '../lib/tauri';
import { grpcRequestsQueryKey } from './useGrpcRequests';
export function useUpdateAnyGrpcRequest() {
const queryClient = useQueryClient();
return useMutation<
void,
unknown,
@@ -23,14 +20,5 @@ export function useUpdateAnyGrpcRequest() {
typeof update === 'function' ? update(request) : { ...request, ...update };
await invokeCmd('cmd_update_grpc_request', { request: patchedRequest });
},
onMutate: async ({ id, update }) => {
const request = await getGrpcRequest(id);
if (request === null) return;
const patchedRequest =
typeof update === 'function' ? update(request) : { ...request, ...update };
queryClient.setQueryData<GrpcRequest[]>(grpcRequestsQueryKey(request), (requests) =>
(requests ?? []).map((r) => (r.id === patchedRequest.id ? patchedRequest : r)),
);
},
});
}

View File

@@ -1,12 +1,9 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useMutation } from '@tanstack/react-query';
import type { HttpRequest } from '@yaakapp/api';
import { getHttpRequest } from '../lib/store';
import { invokeCmd } from '../lib/tauri';
import { httpRequestsQueryKey } from './useHttpRequests';
export function useUpdateAnyHttpRequest() {
const queryClient = useQueryClient();
return useMutation<
void,
unknown,
@@ -23,14 +20,5 @@ export function useUpdateAnyHttpRequest() {
typeof update === 'function' ? update(request) : { ...request, ...update };
await invokeCmd('cmd_update_http_request', { request: patchedRequest });
},
onMutate: async ({ id, update }) => {
const request = await getHttpRequest(id);
if (request === null) return;
const patchedRequest =
typeof update === 'function' ? update(request) : { ...request, ...update };
queryClient.setQueryData<HttpRequest[]>(httpRequestsQueryKey(request), (requests) =>
(requests ?? []).map((r) => (r.id === patchedRequest.id ? patchedRequest : r)),
);
},
});
}

View File

@@ -1,11 +1,9 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useMutation } from '@tanstack/react-query';
import type { CookieJar } from '../lib/models';
import { getCookieJar } from '../lib/store';
import { invokeCmd } from '../lib/tauri';
import { cookieJarsQueryKey } from './useCookieJars';
export function useUpdateCookieJar(id: string | null) {
const queryClient = useQueryClient();
return useMutation<void, unknown, Partial<CookieJar> | ((j: CookieJar) => CookieJar)>({
mutationKey: ['update_cookie_jar', id],
mutationFn: async (v) => {
@@ -15,17 +13,7 @@ export function useUpdateCookieJar(id: string | null) {
}
const newCookieJar = typeof v === 'function' ? v(cookieJar) : { ...cookieJar, ...v };
console.log('NEW COOKIE JAR', newCookieJar.cookies.length);
await invokeCmd('cmd_update_cookie_jar', { cookieJar: newCookieJar });
},
onMutate: async (v) => {
const cookieJar = await getCookieJar(id);
if (cookieJar === null) return;
const newCookieJar = typeof v === 'function' ? v(cookieJar) : { ...cookieJar, ...v };
queryClient.setQueryData<CookieJar[]>(cookieJarsQueryKey(cookieJar), (cookieJars) =>
(cookieJars ?? []).map((j) => (j.id === newCookieJar.id ? newCookieJar : j)),
);
},
});
}

View File

@@ -1,11 +1,9 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useMutation } from '@tanstack/react-query';
import type { Environment } from '@yaakapp/api';
import { getEnvironment } from '../lib/store';
import { invokeCmd } from '../lib/tauri';
import { environmentsQueryKey } from './useEnvironments';
export function useUpdateEnvironment(id: string | null) {
const queryClient = useQueryClient();
return useMutation<void, unknown, Partial<Environment> | ((r: Environment) => Environment)>({
mutationKey: ['update_environment', id],
mutationFn: async (v) => {
@@ -17,14 +15,5 @@ export function useUpdateEnvironment(id: string | null) {
const newEnvironment = typeof v === 'function' ? v(environment) : { ...environment, ...v };
await invokeCmd('cmd_update_environment', { environment: newEnvironment });
},
onMutate: async (v) => {
const environment = await getEnvironment(id);
if (environment === null) return;
const newEnvironment = typeof v === 'function' ? v(environment) : { ...environment, ...v };
queryClient.setQueryData<Environment[]>(environmentsQueryKey(environment), (environments) =>
(environments ?? []).map((r) => (r.id === newEnvironment.id ? newEnvironment : r)),
);
},
});
}

Some files were not shown because too many files have changed in this diff Show More