mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-23 01:08:28 +02:00
Add hotkey dialog and rust-only analytics
This commit is contained in:
@@ -1,89 +1,109 @@
|
|||||||
use log::{debug, warn};
|
use log::{debug, warn};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::types::JsonValue;
|
use sqlx::types::JsonValue;
|
||||||
use tauri::{async_runtime, AppHandle, Manager};
|
use tauri::{async_runtime, AppHandle, Manager};
|
||||||
|
|
||||||
use crate::is_dev;
|
use crate::is_dev;
|
||||||
|
|
||||||
|
// serializable
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
pub enum AnalyticsResource {
|
pub enum AnalyticsResource {
|
||||||
App,
|
App,
|
||||||
// Workspace,
|
Workspace,
|
||||||
// Environment,
|
Environment,
|
||||||
// Folder,
|
Folder,
|
||||||
// HttpRequest,
|
HttpRequest,
|
||||||
// HttpResponse,
|
HttpResponse,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
pub enum AnalyticsAction {
|
pub enum AnalyticsAction {
|
||||||
Launch,
|
Launch,
|
||||||
// Create,
|
Create,
|
||||||
// Update,
|
Update,
|
||||||
// Upsert,
|
Upsert,
|
||||||
// Delete,
|
Delete,
|
||||||
// Send,
|
DeleteMany,
|
||||||
// Duplicate,
|
Send,
|
||||||
|
Duplicate,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resource_name(resource: AnalyticsResource) -> &'static str {
|
fn resource_name(resource: AnalyticsResource) -> &'static str {
|
||||||
match resource {
|
match resource {
|
||||||
AnalyticsResource::App => "app",
|
AnalyticsResource::App => "app",
|
||||||
// AnalyticsResource::Workspace => "workspace",
|
AnalyticsResource::Workspace => "workspace",
|
||||||
// AnalyticsResource::Environment => "environment",
|
AnalyticsResource::Environment => "environment",
|
||||||
// AnalyticsResource::Folder => "folder",
|
AnalyticsResource::Folder => "folder",
|
||||||
// AnalyticsResource::HttpRequest => "http_request",
|
AnalyticsResource::HttpRequest => "http_request",
|
||||||
// AnalyticsResource::HttpResponse => "http_response",
|
AnalyticsResource::HttpResponse => "http_response",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn action_name(action: AnalyticsAction) -> &'static str {
|
fn action_name(action: AnalyticsAction) -> &'static str {
|
||||||
match action {
|
match action {
|
||||||
AnalyticsAction::Launch => "launch",
|
AnalyticsAction::Launch => "launch",
|
||||||
// AnalyticsAction::Create => "create",
|
AnalyticsAction::Create => "create",
|
||||||
// AnalyticsAction::Update => "update",
|
AnalyticsAction::Update => "update",
|
||||||
// AnalyticsAction::Upsert => "upsert",
|
AnalyticsAction::Upsert => "upsert",
|
||||||
// AnalyticsAction::Delete => "delete",
|
AnalyticsAction::Delete => "delete",
|
||||||
// AnalyticsAction::Send => "send",
|
AnalyticsAction::DeleteMany => "delete_many",
|
||||||
// AnalyticsAction::Duplicate => "duplicate",
|
AnalyticsAction::Send => "send",
|
||||||
|
AnalyticsAction::Duplicate => "duplicate",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn track_event(
|
pub fn track_event_blocking(
|
||||||
app_handle: &AppHandle,
|
app_handle: &AppHandle,
|
||||||
resource: AnalyticsResource,
|
resource: AnalyticsResource,
|
||||||
action: AnalyticsAction,
|
action: AnalyticsAction,
|
||||||
attributes: Option<JsonValue>,
|
attributes: Option<JsonValue>,
|
||||||
) {
|
) {
|
||||||
async_runtime::block_on(async move {
|
async_runtime::block_on(async move {
|
||||||
let event = format!("{}.{}", resource_name(resource), action_name(action));
|
track_event(app_handle, resource, action, attributes).await;
|
||||||
let attributes_json = attributes.unwrap_or("{}".to_string().into()).to_string();
|
});
|
||||||
let info = app_handle.package_info();
|
}
|
||||||
let tz = datetime::sys_timezone().unwrap_or("unknown".to_string());
|
|
||||||
let params = vec![
|
|
||||||
("e", event.clone()),
|
|
||||||
("a", attributes_json.clone()),
|
|
||||||
("id", "site_zOK0d7jeBy2TLxFCnZ".to_string()),
|
|
||||||
("v", info.version.clone().to_string()),
|
|
||||||
("os", get_os().to_string()),
|
|
||||||
("tz", tz),
|
|
||||||
("xy", get_window_size(app_handle)),
|
|
||||||
];
|
|
||||||
let url = "https://t.yaak.app/t/e".to_string();
|
|
||||||
let req = reqwest::Client::builder()
|
|
||||||
.build()
|
|
||||||
.unwrap()
|
|
||||||
.get(&url)
|
|
||||||
.query(¶ms);
|
|
||||||
|
|
||||||
if is_dev() {
|
pub async fn track_event(
|
||||||
debug!("Send event (dev): {}", event);
|
app_handle: &AppHandle,
|
||||||
} else if let Err(e) = req.send().await {
|
resource: AnalyticsResource,
|
||||||
warn!(
|
action: AnalyticsAction,
|
||||||
|
attributes: Option<JsonValue>,
|
||||||
|
) {
|
||||||
|
let event = format!("{}.{}", resource_name(resource), action_name(action));
|
||||||
|
let attributes_json = attributes.unwrap_or("{}".to_string().into()).to_string();
|
||||||
|
let info = app_handle.package_info();
|
||||||
|
let tz = datetime::sys_timezone().unwrap_or("unknown".to_string());
|
||||||
|
let site = match is_dev() {
|
||||||
|
true => "site_TkHWjoXwZPq3HfhERb",
|
||||||
|
false => "site_zOK0d7jeBy2TLxFCnZ",
|
||||||
|
};
|
||||||
|
let base_url = match is_dev() {
|
||||||
|
true => "http://localhost:7194",
|
||||||
|
false => "https://t.yaak.app"
|
||||||
|
};
|
||||||
|
let params = vec![
|
||||||
|
("e", event.clone()),
|
||||||
|
("a", attributes_json.clone()),
|
||||||
|
("id", site.to_string()),
|
||||||
|
("v", info.version.clone().to_string()),
|
||||||
|
("os", get_os().to_string()),
|
||||||
|
("tz", tz),
|
||||||
|
("xy", get_window_size(app_handle)),
|
||||||
|
];
|
||||||
|
let req = reqwest::Client::builder()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.get(format!("{base_url}/t/e"))
|
||||||
|
.query(¶ms);
|
||||||
|
|
||||||
|
if let Err(e) = req.send().await {
|
||||||
|
warn!(
|
||||||
"Error sending analytics event: {} {} {:?}",
|
"Error sending analytics event: {} {} {:?}",
|
||||||
e, event, params
|
e, event, params
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
debug!("Send event: {}: {:?}", event, params);
|
debug!("Send event: {}: {:?}", event, params);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_os() -> &'static str {
|
fn get_os() -> &'static str {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use fern::colors::ColoredLevelConfig;
|
|||||||
use log::{debug, info, warn};
|
use log::{debug, info, warn};
|
||||||
use rand::random;
|
use rand::random;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use serde_json::Value;
|
||||||
use sqlx::{Pool, Sqlite, SqlitePool};
|
use sqlx::{Pool, Sqlite, SqlitePool};
|
||||||
use sqlx::migrate::Migrator;
|
use sqlx::migrate::Migrator;
|
||||||
use sqlx::types::Json;
|
use sqlx::types::Json;
|
||||||
@@ -29,7 +30,7 @@ use tokio::sync::Mutex;
|
|||||||
|
|
||||||
use window_ext::TrafficLightWindowExt;
|
use window_ext::TrafficLightWindowExt;
|
||||||
|
|
||||||
use crate::analytics::{AnalyticsAction, AnalyticsResource, track_event};
|
use crate::analytics::{AnalyticsAction, AnalyticsResource};
|
||||||
use crate::plugin::{ImportResources, ImportResult};
|
use crate::plugin::{ImportResources, ImportResult};
|
||||||
use crate::send::actually_send_request;
|
use crate::send::actually_send_request;
|
||||||
use crate::updates::{update_mode_from_str, UpdateMode, YaakUpdater};
|
use crate::updates::{update_mode_from_str, UpdateMode, YaakUpdater};
|
||||||
@@ -224,6 +225,22 @@ async fn response_err(
|
|||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn track_event(
|
||||||
|
window: Window<Wry>,
|
||||||
|
resource: AnalyticsResource,
|
||||||
|
action: AnalyticsAction,
|
||||||
|
attributes: Option<Value>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
analytics::track_event(
|
||||||
|
&window.app_handle(),
|
||||||
|
resource,
|
||||||
|
action,
|
||||||
|
attributes,
|
||||||
|
).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn set_update_mode(
|
async fn set_update_mode(
|
||||||
update_mode: &str,
|
update_mode: &str,
|
||||||
@@ -726,6 +743,7 @@ fn main() {
|
|||||||
send_request,
|
send_request,
|
||||||
set_key_value,
|
set_key_value,
|
||||||
set_update_mode,
|
set_update_mode,
|
||||||
|
track_event,
|
||||||
update_environment,
|
update_environment,
|
||||||
update_folder,
|
update_folder,
|
||||||
update_request,
|
update_request,
|
||||||
@@ -762,7 +780,7 @@ fn main() {
|
|||||||
w.restore_state(StateFlags::all())
|
w.restore_state(StateFlags::all())
|
||||||
.expect("Failed to restore window state");
|
.expect("Failed to restore window state");
|
||||||
|
|
||||||
track_event(
|
analytics::track_event_blocking(
|
||||||
app_handle,
|
app_handle,
|
||||||
AnalyticsResource::App,
|
AnalyticsResource::App,
|
||||||
AnalyticsAction::Launch,
|
AnalyticsAction::Launch,
|
||||||
|
|||||||
@@ -35,8 +35,11 @@ pub async fn actually_send_request(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.redirect(Policy::none())
|
.redirect(Policy::none()) // TODO: Handle redirect manually
|
||||||
// .danger_accept_invalid_certs(true)
|
.danger_accept_invalid_certs(false) // TODO: Make this configurable
|
||||||
|
.connection_verbose(true) // TODO: Capture this log somehow
|
||||||
|
.tls_info(true) // TODO: Capture this log somehow
|
||||||
|
// .use_rustls_tls() // TODO: Make this configurable
|
||||||
.build()
|
.build()
|
||||||
.expect("Failed to build client");
|
.expect("Failed to build client");
|
||||||
|
|
||||||
@@ -237,6 +240,9 @@ pub async fn actually_send_request(
|
|||||||
}
|
}
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
Err(e) => response_err(response, e.to_string(), app_handle, pool).await,
|
Err(e) => {
|
||||||
|
println!("Yo: {}", e);
|
||||||
|
response_err(response, e.to_string(), app_handle, pool).await
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useQueryClient } from '@tanstack/react-query';
|
|||||||
import { appWindow } from '@tauri-apps/api/window';
|
import { appWindow } from '@tauri-apps/api/window';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { useEffectOnce } from 'react-use';
|
|
||||||
import { keyValueQueryKey } from '../hooks/useKeyValue';
|
import { keyValueQueryKey } from '../hooks/useKeyValue';
|
||||||
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
||||||
import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
|
import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
|
||||||
@@ -13,7 +12,6 @@ import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
|||||||
import { responsesQueryKey } from '../hooks/useResponses';
|
import { responsesQueryKey } from '../hooks/useResponses';
|
||||||
import { useSyncWindowTitle } from '../hooks/useSyncWindowTitle';
|
import { useSyncWindowTitle } from '../hooks/useSyncWindowTitle';
|
||||||
import { workspacesQueryKey } from '../hooks/useWorkspaces';
|
import { workspacesQueryKey } from '../hooks/useWorkspaces';
|
||||||
import { trackPage } from '../lib/analytics';
|
|
||||||
import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore';
|
import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore';
|
||||||
import type { HttpRequest, HttpResponse, Model, Workspace } from '../lib/models';
|
import type { HttpRequest, HttpResponse, Model, Workspace } from '../lib/models';
|
||||||
import { modelsEq } from '../lib/models';
|
import { modelsEq } from '../lib/models';
|
||||||
@@ -39,10 +37,6 @@ export function GlobalHooks() {
|
|||||||
setPathname(location.pathname).catch(console.error);
|
setPathname(location.pathname).catch(console.error);
|
||||||
}, [location.pathname]);
|
}, [location.pathname]);
|
||||||
|
|
||||||
useEffectOnce(() => {
|
|
||||||
trackPage('/');
|
|
||||||
});
|
|
||||||
|
|
||||||
useListenToTauriEvent<Model>('created_model', ({ payload, windowLabel }) => {
|
useListenToTauriEvent<Model>('created_model', ({ payload, windowLabel }) => {
|
||||||
if (shouldIgnoreEvent(payload, windowLabel)) return;
|
if (shouldIgnoreEvent(payload, windowLabel)) return;
|
||||||
|
|
||||||
|
|||||||
10
src-web/components/KeyboardShortcutsDialog.tsx
Normal file
10
src-web/components/KeyboardShortcutsDialog.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { hotkeyActions } from '../hooks/useHotkey';
|
||||||
|
import { HotKeyList } from './core/HotKeyList';
|
||||||
|
|
||||||
|
export const KeyboardShortcutsDialog = () => {
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full">
|
||||||
|
<HotKeyList hotkeys={hotkeyActions} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -12,6 +12,7 @@ import { Icon } from './core/Icon';
|
|||||||
import { IconButton } from './core/IconButton';
|
import { IconButton } from './core/IconButton';
|
||||||
import { VStack } from './core/Stacks';
|
import { VStack } from './core/Stacks';
|
||||||
import { useDialog } from './DialogContext';
|
import { useDialog } from './DialogContext';
|
||||||
|
import { KeyboardShortcutsDialog } from './KeyboardShortcutsDialog';
|
||||||
|
|
||||||
export function SettingsDropdown() {
|
export function SettingsDropdown() {
|
||||||
const importData = useImportData();
|
const importData = useImportData();
|
||||||
@@ -66,7 +67,18 @@ export function SettingsDropdown() {
|
|||||||
onSelect: toggleAppearance,
|
onSelect: toggleAppearance,
|
||||||
leftSlot: <Icon icon={appearance === 'dark' ? 'sun' : 'moon'} />,
|
leftSlot: <Icon icon={appearance === 'dark' ? 'sun' : 'moon'} />,
|
||||||
},
|
},
|
||||||
{ type: 'separator', label: `v${appVersion.data}` },
|
{
|
||||||
|
key: 'hotkeys',
|
||||||
|
label: 'Keyboard shortcuts',
|
||||||
|
onSelect: () =>
|
||||||
|
dialog.show({
|
||||||
|
title: 'Keyboard Shortcuts',
|
||||||
|
size: 'dynamic',
|
||||||
|
render: () => <KeyboardShortcutsDialog />,
|
||||||
|
}),
|
||||||
|
leftSlot: <Icon icon="keyboard" />,
|
||||||
|
},
|
||||||
|
{ type: 'separator', label: `Yaak v${appVersion.data}` },
|
||||||
{
|
{
|
||||||
key: 'update-mode',
|
key: 'update-mode',
|
||||||
label: updateMode === 'stable' ? 'Enable Beta' : 'Disable Beta',
|
label: updateMode === 'stable' ? 'Enable Beta' : 'Disable Beta',
|
||||||
|
|||||||
@@ -290,27 +290,13 @@ function getExtensions({
|
|||||||
|
|
||||||
// Handle onChange
|
// Handle onChange
|
||||||
EditorView.updateListener.of((update) => {
|
EditorView.updateListener.of((update) => {
|
||||||
if (onChange && update.docChanged && isViewUpdateFromUserInput(update)) {
|
if (onChange && update.docChanged) {
|
||||||
onChange.current?.(update.state.doc.toString());
|
onChange.current?.(update.state.doc.toString());
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function isViewUpdateFromUserInput(viewUpdate: ViewUpdate) {
|
|
||||||
// Make sure document has changed, ensuring user events like selections don't count.
|
|
||||||
if (viewUpdate.docChanged) {
|
|
||||||
// Check transactions for any that are direct user input, not changes from Y.js or another extension.
|
|
||||||
for (const transaction of viewUpdate.transactions) {
|
|
||||||
// Not using Transaction.isUserEvent because that only checks for a specific User event type ( "input", "delete", etc.). Checking the annotation directly allows for any type of user event.
|
|
||||||
const userEventType = transaction.annotation(Transaction.userEvent);
|
|
||||||
if (userEventType) return userEventType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const syncGutterBg = ({
|
const syncGutterBg = ({
|
||||||
parent,
|
parent,
|
||||||
className = '',
|
className = '',
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const icons = {
|
|||||||
gear: I.GearIcon,
|
gear: I.GearIcon,
|
||||||
hamburger: I.HamburgerMenuIcon,
|
hamburger: I.HamburgerMenuIcon,
|
||||||
home: I.HomeIcon,
|
home: I.HomeIcon,
|
||||||
|
keyboard: I.KeyboardIcon,
|
||||||
listBullet: I.ListBulletIcon,
|
listBullet: I.ListBulletIcon,
|
||||||
magicWand: I.MagicWandIcon,
|
magicWand: I.MagicWandIcon,
|
||||||
magnifyingGlass: I.MagnifyingGlassIcon,
|
magnifyingGlass: I.MagnifyingGlassIcon,
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ export function useCreateEnvironment() {
|
|||||||
const prompt = usePrompt();
|
const prompt = usePrompt();
|
||||||
const workspaceId = useActiveWorkspaceId();
|
const workspaceId = useActiveWorkspaceId();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const environments = useEnvironments();
|
|
||||||
const workspaces = useWorkspaces();
|
|
||||||
|
|
||||||
return useMutation<Environment, unknown, void>({
|
return useMutation<Environment, unknown, void>({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
@@ -26,7 +24,7 @@ export function useCreateEnvironment() {
|
|||||||
});
|
});
|
||||||
return invoke('create_environment', { name, variables: [], workspaceId });
|
return invoke('create_environment', { name, variables: [], workspaceId });
|
||||||
},
|
},
|
||||||
onSettled: () => trackEvent('environment', 'create'),
|
onSettled: () => trackEvent('Environment', 'Create'),
|
||||||
onSuccess: async (environment) => {
|
onSuccess: async (environment) => {
|
||||||
if (workspaceId == null) return;
|
if (workspaceId == null) return;
|
||||||
routes.setEnvironment(environment);
|
routes.setEnvironment(environment);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export function useCreateFolder() {
|
|||||||
patch.sortPriority = patch.sortPriority || -Date.now();
|
patch.sortPriority = patch.sortPriority || -Date.now();
|
||||||
return invoke('create_folder', { workspaceId, ...patch });
|
return invoke('create_folder', { workspaceId, ...patch });
|
||||||
},
|
},
|
||||||
onSettled: () => trackEvent('folder', 'create'),
|
onSettled: () => trackEvent('Folder', 'Create'),
|
||||||
onSuccess: async (request) => {
|
onSuccess: async (request) => {
|
||||||
await queryClient.invalidateQueries(foldersQueryKey({ workspaceId: request.workspaceId }));
|
await queryClient.invalidateQueries(foldersQueryKey({ workspaceId: request.workspaceId }));
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export function useCreateRequest() {
|
|||||||
patch.folderId = patch.folderId || activeRequest?.folderId;
|
patch.folderId = patch.folderId || activeRequest?.folderId;
|
||||||
return invoke('create_request', { workspaceId, name: '', ...patch });
|
return invoke('create_request', { workspaceId, name: '', ...patch });
|
||||||
},
|
},
|
||||||
onSettled: () => trackEvent('http_request', 'create'),
|
onSettled: () => trackEvent('HttpRequest', 'Create'),
|
||||||
onSuccess: async (request) => {
|
onSuccess: async (request) => {
|
||||||
queryClient.setQueryData<HttpRequest[]>(
|
queryClient.setQueryData<HttpRequest[]>(
|
||||||
requestsQueryKey({ workspaceId: request.workspaceId }),
|
requestsQueryKey({ workspaceId: request.workspaceId }),
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export function useCreateWorkspace({ navigateAfter }: { navigateAfter: boolean }
|
|||||||
mutationFn: (patch) => {
|
mutationFn: (patch) => {
|
||||||
return invoke('create_workspace', patch);
|
return invoke('create_workspace', patch);
|
||||||
},
|
},
|
||||||
onSettled: () => trackEvent('workspace', 'create'),
|
onSettled: () => trackEvent('Workspace', 'Create'),
|
||||||
onSuccess: async (workspace) => {
|
onSuccess: async (workspace) => {
|
||||||
queryClient.setQueryData<Workspace[]>(workspacesQueryKey({}), (workspaces) => [
|
queryClient.setQueryData<Workspace[]>(workspacesQueryKey({}), (workspaces) => [
|
||||||
...(workspaces ?? []),
|
...(workspaces ?? []),
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export function useDeleteAnyRequest() {
|
|||||||
if (!confirmed) return null;
|
if (!confirmed) return null;
|
||||||
return invoke('delete_request', { requestId: id });
|
return invoke('delete_request', { requestId: id });
|
||||||
},
|
},
|
||||||
onSettled: () => trackEvent('http_request', 'delete'),
|
onSettled: () => trackEvent('HttpRequest', 'Delete'),
|
||||||
onSuccess: async (request) => {
|
onSuccess: async (request) => {
|
||||||
// Was it cancelled?
|
// Was it cancelled?
|
||||||
if (request === null) return;
|
if (request === null) return;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function useDeleteEnvironment(environment: Environment | null) {
|
|||||||
if (!confirmed) return null;
|
if (!confirmed) return null;
|
||||||
return invoke('delete_environment', { environmentId: environment?.id });
|
return invoke('delete_environment', { environmentId: environment?.id });
|
||||||
},
|
},
|
||||||
onSettled: () => trackEvent('environment', 'delete'),
|
onSettled: () => trackEvent('Environment', 'Delete'),
|
||||||
onSuccess: async (environment) => {
|
onSuccess: async (environment) => {
|
||||||
if (environment === null) return;
|
if (environment === null) return;
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function useDeleteFolder(id: string | null) {
|
|||||||
if (!confirmed) return null;
|
if (!confirmed) return null;
|
||||||
return invoke('delete_folder', { folderId: id });
|
return invoke('delete_folder', { folderId: id });
|
||||||
},
|
},
|
||||||
onSettled: () => trackEvent('folder', 'delete'),
|
onSettled: () => trackEvent('Folder', 'Delete'),
|
||||||
onSuccess: async (folder) => {
|
onSuccess: async (folder) => {
|
||||||
// Was it cancelled?
|
// Was it cancelled?
|
||||||
if (folder === null) return;
|
if (folder === null) return;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export function useDeleteResponse(id: string | null) {
|
|||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
return await invoke('delete_response', { id: id });
|
return await invoke('delete_response', { id: id });
|
||||||
},
|
},
|
||||||
onSettled: () => trackEvent('http_response', 'delete'),
|
onSettled: () => trackEvent('HttpResponse', 'Delete'),
|
||||||
onSuccess: ({ requestId, id: responseId }) => {
|
onSuccess: ({ requestId, id: responseId }) => {
|
||||||
queryClient.setQueryData<HttpResponse[]>(responsesQueryKey({ requestId }), (responses) =>
|
queryClient.setQueryData<HttpResponse[]>(responsesQueryKey({ requestId }), (responses) =>
|
||||||
(responses ?? []).filter((response) => response.id !== responseId),
|
(responses ?? []).filter((response) => response.id !== responseId),
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export function useDeleteResponses(requestId?: string) {
|
|||||||
if (requestId === undefined) return;
|
if (requestId === undefined) return;
|
||||||
await invoke('delete_all_responses', { requestId });
|
await invoke('delete_all_responses', { requestId });
|
||||||
},
|
},
|
||||||
onSettled: () => trackEvent('http_response', 'delete_many'),
|
onSettled: () => trackEvent('HttpResponse', 'DeleteMany'),
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
if (requestId === undefined) return;
|
if (requestId === undefined) return;
|
||||||
queryClient.setQueryData(responsesQueryKey({ requestId }), []);
|
queryClient.setQueryData(responsesQueryKey({ requestId }), []);
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export function useDeleteWorkspace(workspace: Workspace | null) {
|
|||||||
if (!confirmed) return null;
|
if (!confirmed) return null;
|
||||||
return invoke('delete_workspace', { workspaceId: workspace?.id });
|
return invoke('delete_workspace', { workspaceId: workspace?.id });
|
||||||
},
|
},
|
||||||
onSettled: () => trackEvent('workspace', 'delete'),
|
onSettled: () => trackEvent('Workspace', 'Delete'),
|
||||||
onSuccess: async (workspace) => {
|
onSuccess: async (workspace) => {
|
||||||
if (workspace === null) return;
|
if (workspace === null) return;
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export function useDuplicateRequest({
|
|||||||
if (id === null) throw new Error("Can't duplicate a null request");
|
if (id === null) throw new Error("Can't duplicate a null request");
|
||||||
return invoke('duplicate_request', { id });
|
return invoke('duplicate_request', { id });
|
||||||
},
|
},
|
||||||
onSettled: () => trackEvent('http_request', 'duplicate'),
|
onSettled: () => trackEvent('HttpRequest', 'Duplicate'),
|
||||||
onSuccess: async (request) => {
|
onSuccess: async (request) => {
|
||||||
queryClient.setQueryData<HttpRequest[]>(
|
queryClient.setQueryData<HttpRequest[]>(
|
||||||
requestsQueryKey({ workspaceId: request.workspaceId }),
|
requestsQueryKey({ workspaceId: request.workspaceId }),
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ const hotkeys: Record<HotkeyAction, string[]> = {
|
|||||||
'environmentEditor.toggle': ['CmdCtrl+e'],
|
'environmentEditor.toggle': ['CmdCtrl+e'],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const hotkeyActions: HotkeyAction[] = Object.keys(hotkeys) as (keyof typeof hotkeys)[];
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
enable?: boolean;
|
enable?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export function useSendAnyRequest() {
|
|||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
return useMutation<HttpResponse, string, string | null>({
|
return useMutation<HttpResponse, string, string | null>({
|
||||||
mutationFn: (id) => invoke('send_request', { requestId: id, environmentId }),
|
mutationFn: (id) => invoke('send_request', { requestId: id, environmentId }),
|
||||||
onSettled: () => trackEvent('http_request', 'send'),
|
onSettled: () => trackEvent('HttpRequest', 'Send'),
|
||||||
onError: (err) => alert({ title: 'Export Failed', body: err }),
|
onError: (err) => alert({ title: 'Export Failed', body: err }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,5 @@ export function useSendManyRequests() {
|
|||||||
sendAnyRequest.mutate(id);
|
sendAnyRequest.mutate(id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSettled: () => trackEvent('http_request', 'send'),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +1,20 @@
|
|||||||
import { getVersion } from '@tauri-apps/api/app';
|
import { invoke } from '@tauri-apps/api';
|
||||||
import type { Environment, Folder, HttpRequest, HttpResponse, KeyValue, Workspace } from './models';
|
|
||||||
|
|
||||||
const appVersion = await getVersion();
|
|
||||||
|
|
||||||
export function trackEvent(
|
export function trackEvent(
|
||||||
resource:
|
resource:
|
||||||
| Workspace['model']
|
| 'App'
|
||||||
| Environment['model']
|
| 'Workspace'
|
||||||
| Folder['model']
|
| 'Environment'
|
||||||
| HttpRequest['model']
|
| 'Folder'
|
||||||
| HttpResponse['model']
|
| 'HttpRequest'
|
||||||
| KeyValue['model'],
|
| 'HttpResponse'
|
||||||
event: 'create' | 'update' | 'delete' | 'delete_many' | 'send' | 'duplicate',
|
| 'KeyValue',
|
||||||
|
action: 'Launch' | 'Create' | 'Update' | 'Delete' | 'DeleteMany' | 'Send' | 'Duplicate',
|
||||||
attributes: Record<string, string | number> = {},
|
attributes: Record<string, string | number> = {},
|
||||||
) {
|
) {
|
||||||
send('/e', [
|
invoke('track_event', {
|
||||||
{ name: 'e', value: `${resource}.${event}` },
|
resource: resource,
|
||||||
{ name: 'a', value: JSON.stringify({ ...attributes, version: appVersion }) },
|
action,
|
||||||
]);
|
attributes,
|
||||||
}
|
}).catch(console.error);
|
||||||
|
|
||||||
export function trackPage(pathname: string) {
|
|
||||||
if (pathname === sessionStorage.lastPathName) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionStorage.lastPathName = pathname;
|
|
||||||
send('/p', [
|
|
||||||
{
|
|
||||||
name: 'h',
|
|
||||||
value: 'desktop.yaak.app',
|
|
||||||
},
|
|
||||||
{ name: 'p', value: pathname },
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function send(path: string, params: { name: string; value: string | number }[]) {
|
|
||||||
if (localStorage.disableAnalytics === 'true') {
|
|
||||||
console.log('Analytics disabled', path, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
params.push({ name: 'id', value: 'site_zOK0d7jeBy2TLxFCnZ' });
|
|
||||||
params.push({
|
|
||||||
name: 'tz',
|
|
||||||
value: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
||||||
});
|
|
||||||
params.push({ name: 'xy', value: screensize() });
|
|
||||||
const qs = params.map((v) => `${v.name}=${encodeURIComponent(v.value)}`).join('&');
|
|
||||||
const url = `https://t.yaak.app/t${path}?${qs}`;
|
|
||||||
fetch(url, { mode: 'no-cors' }).catch((err) => console.log('Error:', err));
|
|
||||||
}
|
|
||||||
|
|
||||||
function screensize() {
|
|
||||||
const w = window.screen.width;
|
|
||||||
const h = window.screen.height;
|
|
||||||
return `${Math.round(w / 100) * 100}x${Math.round(h / 100) * 100}`;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user