mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-21 16:21:25 +02:00
Better insight into settings updates
This commit is contained in:
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
@@ -8000,6 +8000,7 @@ dependencies = [
|
|||||||
"tauri-plugin-yaak-license",
|
"tauri-plugin-yaak-license",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
|
"ts-rs",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
"uuid",
|
"uuid",
|
||||||
"yaak_grpc",
|
"yaak_grpc",
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ tauri-plugin-updater = "2.0.2"
|
|||||||
tauri-plugin-window-state = "2.0.1"
|
tauri-plugin-window-state = "2.0.1"
|
||||||
tokio = { version = "1.36.0", features = ["sync"] }
|
tokio = { version = "1.36.0", features = ["sync"] }
|
||||||
tokio-stream = "0.1.15"
|
tokio-stream = "0.1.15"
|
||||||
|
ts-rs = { workspace = true }
|
||||||
uuid = "1.7.0"
|
uuid = "1.7.0"
|
||||||
mime_guess = "2.0.5"
|
mime_guess = "2.0.5"
|
||||||
urlencoding = "2.1.3"
|
urlencoding = "2.1.3"
|
||||||
|
|||||||
5
src-tauri/bindings/analytics.ts
Normal file
5
src-tauri/bindings/analytics.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type AnalyticsAction = "cancel" | "click" | "commit" | "create" | "delete" | "delete_many" | "duplicate" | "export" | "hide" | "import" | "launch" | "launch_first" | "launch_update" | "send" | "show" | "toggle" | "update" | "upsert";
|
||||||
|
|
||||||
|
export type AnalyticsResource = "app" | "appearance" | "button" | "checkbox" | "cookie_jar" | "dialog" | "environment" | "folder" | "grpc_connection" | "grpc_event" | "grpc_request" | "http_request" | "http_response" | "link" | "key_value" | "plugin" | "select" | "setting" | "sidebar" | "tab" | "theme" | "workspace";
|
||||||
@@ -4,7 +4,7 @@ use log::{debug, info};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use tauri::{Manager, Runtime, WebviewWindow};
|
use tauri::{Manager, Runtime, WebviewWindow};
|
||||||
|
use ts_rs::TS;
|
||||||
use yaak_models::queries::{generate_id, get_key_value_int, get_key_value_string, get_or_create_settings, set_key_value_int, set_key_value_string};
|
use yaak_models::queries::{generate_id, get_key_value_int, get_key_value_string, get_or_create_settings, set_key_value_int, set_key_value_string};
|
||||||
|
|
||||||
use crate::is_dev;
|
use crate::is_dev;
|
||||||
@@ -13,11 +13,14 @@ const NAMESPACE: &str = "analytics";
|
|||||||
const NUM_LAUNCHES_KEY: &str = "num_launches";
|
const NUM_LAUNCHES_KEY: &str = "num_launches";
|
||||||
|
|
||||||
// serializable
|
// serializable
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug, TS)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
|
#[ts(export, export_to = "analytics.ts")]
|
||||||
pub enum AnalyticsResource {
|
pub enum AnalyticsResource {
|
||||||
App,
|
App,
|
||||||
Appearance,
|
Appearance,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
CookieJar,
|
CookieJar,
|
||||||
Dialog,
|
Dialog,
|
||||||
Environment,
|
Environment,
|
||||||
@@ -27,10 +30,13 @@ pub enum AnalyticsResource {
|
|||||||
GrpcRequest,
|
GrpcRequest,
|
||||||
HttpRequest,
|
HttpRequest,
|
||||||
HttpResponse,
|
HttpResponse,
|
||||||
|
Link,
|
||||||
KeyValue,
|
KeyValue,
|
||||||
Plugin,
|
Plugin,
|
||||||
|
Select,
|
||||||
Setting,
|
Setting,
|
||||||
Sidebar,
|
Sidebar,
|
||||||
|
Tab,
|
||||||
Theme,
|
Theme,
|
||||||
Workspace,
|
Workspace,
|
||||||
}
|
}
|
||||||
@@ -51,10 +57,12 @@ impl Display for AnalyticsResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug, TS)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
|
#[ts(export, export_to = "analytics.ts")]
|
||||||
pub enum AnalyticsAction {
|
pub enum AnalyticsAction {
|
||||||
Cancel,
|
Cancel,
|
||||||
|
Click,
|
||||||
Commit,
|
Commit,
|
||||||
Create,
|
Create,
|
||||||
Delete,
|
Delete,
|
||||||
@@ -156,7 +164,7 @@ pub async fn track_event<R: Runtime>(
|
|||||||
action: AnalyticsAction,
|
action: AnalyticsAction,
|
||||||
attributes: Option<Value>,
|
attributes: Option<Value>,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
let id = get_id(w).await;
|
let id = get_id(w).await;
|
||||||
let event = format!("{}.{}", resource, action);
|
let event = format!("{}.{}", resource, action);
|
||||||
let attributes_json = attributes.unwrap_or("{}".to_string().into()).to_string();
|
let attributes_json = attributes.unwrap_or("{}".to_string().into()).to_string();
|
||||||
|
|||||||
@@ -8,4 +8,4 @@ export type ActivateLicenseResponsePayload = { activationId: string, };
|
|||||||
|
|
||||||
export type CheckActivationResponsePayload = { active: boolean, };
|
export type CheckActivationResponsePayload = { active: boolean, };
|
||||||
|
|
||||||
export type LicenseCheckStatus = { "type": "personal_use" } | { "type": "commercial_use" } | { "type": "trialing", end: string, } | { "type": "trial_ended", end: string, };
|
export type LicenseCheckStatus = { "type": "personal_use", trial_ended: string, } | { "type": "commercial_use" } | { "type": "invalid_license" } | { "type": "trialing", end: string, };
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ const details: Record<
|
|||||||
{ label: string; color: ButtonProps['color'] } | null
|
{ label: string; color: ButtonProps['color'] } | null
|
||||||
> = {
|
> = {
|
||||||
commercial_use: null,
|
commercial_use: null,
|
||||||
invalid_license: { label: 'Invalid License', color: 'danger' },
|
invalid_license: { label: 'License Error', color: 'danger' },
|
||||||
personal_use: { label: 'Personal Use', color: 'success' },
|
personal_use: { label: 'Personal Use', color: 'primary' },
|
||||||
trialing: { label: 'Personal Use', color: 'success' },
|
trialing: { label: 'Personal Use', color: 'primary' },
|
||||||
};
|
};
|
||||||
|
|
||||||
export function LicenseBadge() {
|
export function LicenseBadge() {
|
||||||
@@ -35,6 +35,7 @@ export function LicenseBadge() {
|
|||||||
className="!rounded-full mx-1"
|
className="!rounded-full mx-1"
|
||||||
onClick={() => openSettings.mutate()}
|
onClick={() => openSettings.mutate()}
|
||||||
color={detail.color}
|
color={detail.color}
|
||||||
|
event={{ id: 'license-badge', status: check.data.type }}
|
||||||
>
|
>
|
||||||
{detail.label}
|
{detail.label}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { useResolvedAppearance } from '../../hooks/useResolvedAppearance';
|
|||||||
import { useResolvedTheme } from '../../hooks/useResolvedTheme';
|
import { useResolvedTheme } from '../../hooks/useResolvedTheme';
|
||||||
import { useSettings } from '../../hooks/useSettings';
|
import { useSettings } from '../../hooks/useSettings';
|
||||||
import { useUpdateSettings } from '../../hooks/useUpdateSettings';
|
import { useUpdateSettings } from '../../hooks/useUpdateSettings';
|
||||||
import { trackEvent } from '../../lib/analytics';
|
|
||||||
import { clamp } from '../../lib/clamp';
|
import { clamp } from '../../lib/clamp';
|
||||||
import { getThemes } from '../../lib/theme/themes';
|
import { getThemes } from '../../lib/theme/themes';
|
||||||
import { isThemeDark } from '../../lib/theme/window';
|
import { isThemeDark } from '../../lib/theme/window';
|
||||||
@@ -89,6 +88,7 @@ export function SettingsAppearance() {
|
|||||||
value={`${settings.interfaceFontSize}`}
|
value={`${settings.interfaceFontSize}`}
|
||||||
options={fontSizes}
|
options={fontSizes}
|
||||||
onChange={(v) => updateSettings.mutate({ interfaceFontSize: parseInt(v) })}
|
onChange={(v) => updateSettings.mutate({ interfaceFontSize: parseInt(v) })}
|
||||||
|
event="font-size.interface"
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -98,11 +98,13 @@ export function SettingsAppearance() {
|
|||||||
value={`${settings.editorFontSize}`}
|
value={`${settings.editorFontSize}`}
|
||||||
options={fontSizes}
|
options={fontSizes}
|
||||||
onChange={(v) => updateSettings.mutate({ editorFontSize: clamp(parseInt(v) || 14, 8, 30) })}
|
onChange={(v) => updateSettings.mutate({ editorFontSize: clamp(parseInt(v) || 14, 8, 30) })}
|
||||||
|
event="font-size.editor"
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={settings.editorSoftWrap}
|
checked={settings.editorSoftWrap}
|
||||||
title="Wrap Editor Lines"
|
title="Wrap Editor Lines"
|
||||||
onChange={(editorSoftWrap) => updateSettings.mutate({ editorSoftWrap })}
|
onChange={(editorSoftWrap) => updateSettings.mutate({ editorSoftWrap })}
|
||||||
|
event="wrap-lines"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Separator className="my-4" />
|
<Separator className="my-4" />
|
||||||
@@ -113,10 +115,8 @@ export function SettingsAppearance() {
|
|||||||
labelPosition="top"
|
labelPosition="top"
|
||||||
size="sm"
|
size="sm"
|
||||||
value={settings.appearance}
|
value={settings.appearance}
|
||||||
onChange={(appearance) => {
|
onChange={(appearance) => updateSettings.mutate({ appearance })}
|
||||||
trackEvent('appearance', 'update', { appearance });
|
event="appearance"
|
||||||
updateSettings.mutateAsync({ appearance });
|
|
||||||
}}
|
|
||||||
options={[
|
options={[
|
||||||
{ label: 'Automatic', value: 'system' },
|
{ label: 'Automatic', value: 'system' },
|
||||||
{ label: 'Light', value: 'light' },
|
{ label: 'Light', value: 'light' },
|
||||||
@@ -134,10 +134,8 @@ export function SettingsAppearance() {
|
|||||||
className="flex-1"
|
className="flex-1"
|
||||||
value={activeTheme.light.id}
|
value={activeTheme.light.id}
|
||||||
options={lightThemes}
|
options={lightThemes}
|
||||||
onChange={(themeLight) => {
|
event="theme.light"
|
||||||
trackEvent('theme', 'update', { theme: themeLight, appearance: 'light' });
|
onChange={(themeLight) => updateSettings.mutate({ ...settings, themeLight })}
|
||||||
updateSettings.mutateAsync({ ...settings, themeLight });
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{(settings.appearance === 'system' || settings.appearance === 'dark') && (
|
{(settings.appearance === 'system' || settings.appearance === 'dark') && (
|
||||||
@@ -150,10 +148,8 @@ export function SettingsAppearance() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
value={activeTheme.dark.id}
|
value={activeTheme.dark.id}
|
||||||
options={darkThemes}
|
options={darkThemes}
|
||||||
onChange={(themeDark) => {
|
event="theme.dark"
|
||||||
trackEvent('theme', 'update', { theme: themeDark, appearance: 'dark' });
|
onChange={(themeDark) => updateSettings.mutate({ ...settings, themeDark })}
|
||||||
updateSettings.mutateAsync({ ...settings, themeDark });
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export function SettingsGeneral() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
value={settings.updateChannel}
|
value={settings.updateChannel}
|
||||||
onChange={(updateChannel) => updateSettings.mutate({ updateChannel })}
|
onChange={(updateChannel) => updateSettings.mutate({ updateChannel })}
|
||||||
|
event="update-channel"
|
||||||
options={[
|
options={[
|
||||||
{ label: 'Stable (less frequent)', value: 'stable' },
|
{ label: 'Stable (less frequent)', value: 'stable' },
|
||||||
{ label: 'Beta (more frequent)', value: 'beta' },
|
{ label: 'Beta (more frequent)', value: 'beta' },
|
||||||
@@ -57,6 +58,7 @@ export function SettingsGeneral() {
|
|||||||
labelPosition="left"
|
labelPosition="left"
|
||||||
labelClassName="w-[12rem]"
|
labelClassName="w-[12rem]"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
event="workspace-open"
|
||||||
value={
|
value={
|
||||||
settings.openWorkspaceNewWindow === true
|
settings.openWorkspaceNewWindow === true
|
||||||
? 'new'
|
? 'new'
|
||||||
@@ -80,6 +82,7 @@ export function SettingsGeneral() {
|
|||||||
className="mt-3"
|
className="mt-3"
|
||||||
checked={settings.telemetry}
|
checked={settings.telemetry}
|
||||||
title="Send Usage Statistics"
|
title="Send Usage Statistics"
|
||||||
|
event="usage-statistics"
|
||||||
onChange={(telemetry) => updateSettings.mutate({ telemetry })}
|
onChange={(telemetry) => updateSettings.mutate({ telemetry })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -107,6 +110,7 @@ export function SettingsGeneral() {
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
checked={workspace.settingValidateCertificates}
|
checked={workspace.settingValidateCertificates}
|
||||||
title="Validate TLS Certificates"
|
title="Validate TLS Certificates"
|
||||||
|
event="validate-certs"
|
||||||
onChange={(settingValidateCertificates) =>
|
onChange={(settingValidateCertificates) =>
|
||||||
updateWorkspace.mutate({ settingValidateCertificates })
|
updateWorkspace.mutate({ settingValidateCertificates })
|
||||||
}
|
}
|
||||||
@@ -115,6 +119,7 @@ export function SettingsGeneral() {
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
checked={workspace.settingFollowRedirects}
|
checked={workspace.settingFollowRedirects}
|
||||||
title="Follow Redirects"
|
title="Follow Redirects"
|
||||||
|
event="follow-redirects"
|
||||||
onChange={(settingFollowRedirects) => updateWorkspace.mutate({ settingFollowRedirects })}
|
onChange={(settingFollowRedirects) => updateWorkspace.mutate({ settingFollowRedirects })}
|
||||||
/>
|
/>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|||||||
@@ -21,29 +21,27 @@ export function SettingsLicense() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
{check.data?.type === 'commercial_use' && (
|
{check.data?.type === 'commercial_use' ? (
|
||||||
<Banner color="success">
|
<Banner color="success">
|
||||||
<strong>License active!</strong> Enjoy using Yaak for commercial use.
|
<strong>License active!</strong> Enjoy using Yaak for commercial use.
|
||||||
</Banner>
|
</Banner>
|
||||||
)}
|
) : (
|
||||||
{check.data?.type === 'trialing' && (
|
|
||||||
<Banner color="success">
|
|
||||||
<strong>Your trial ends in {formatDistanceToNow(check.data.end)}</strong>. If you're
|
|
||||||
using Yaak for commercial use, please purchase a commercial use license.
|
|
||||||
</Banner>
|
|
||||||
)}
|
|
||||||
{check.data?.type === 'personal_use' && (
|
|
||||||
<Banner color="primary" className="flex flex-col gap-2">
|
<Banner color="primary" className="flex flex-col gap-2">
|
||||||
<h2 className="text-lg font-semibold">Commercial License</h2>
|
{check.data?.type === 'trialing' && (
|
||||||
|
<p>
|
||||||
|
<strong>Your trial ends in {formatDistanceToNow(check.data.end)}.</strong>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<p>
|
<p>
|
||||||
A commercial license is required if you use Yaak within a for-profit organization of two
|
A commercial license is required if using Yaak within a for-profit organization of two
|
||||||
or more people.
|
or more people. This helps support the ongoing development of Yaak and ensures continued
|
||||||
</p>
|
growth and improvement.
|
||||||
<p>
|
|
||||||
<Link href="https://yaak.app/pricing" className="text-sm">
|
|
||||||
Learn More
|
|
||||||
</Link>
|
|
||||||
</p>
|
</p>
|
||||||
|
<p>If you're using Yaak for personal use, no action is needed.</p>
|
||||||
|
<p>~ Gregory</p>
|
||||||
|
<Link href="https://yaak.app/pricing" className="text-sm text-text-subtle">
|
||||||
|
Learn More
|
||||||
|
</Link>
|
||||||
</Banner>
|
</Banner>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -52,7 +50,13 @@ export function SettingsLicense() {
|
|||||||
|
|
||||||
{check.data?.type === 'commercial_use' ? (
|
{check.data?.type === 'commercial_use' ? (
|
||||||
<HStack space={2}>
|
<HStack space={2}>
|
||||||
<Button variant="border" color="secondary" size="sm" onClick={toggleActivateFormVisible}>
|
<Button
|
||||||
|
variant="border"
|
||||||
|
color="secondary"
|
||||||
|
size="sm"
|
||||||
|
onClick={toggleActivateFormVisible}
|
||||||
|
event="license.another"
|
||||||
|
>
|
||||||
Activate Another License
|
Activate Another License
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -60,20 +64,27 @@ export function SettingsLicense() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => open('https://yaak.app/dashboard')}
|
onClick={() => open('https://yaak.app/dashboard')}
|
||||||
rightSlot={<Icon icon="external_link" />}
|
rightSlot={<Icon icon="external_link" />}
|
||||||
|
event="license.support"
|
||||||
>
|
>
|
||||||
Direct Support
|
Direct Support
|
||||||
</Button>
|
</Button>
|
||||||
</HStack>
|
</HStack>
|
||||||
) : (
|
) : (
|
||||||
<HStack space={2}>
|
<HStack space={2}>
|
||||||
<Button color="primary" size="sm" onClick={toggleActivateFormVisible}>
|
<Button
|
||||||
Activate License
|
color="primary"
|
||||||
|
size="sm"
|
||||||
|
onClick={toggleActivateFormVisible}
|
||||||
|
event="license.activate"
|
||||||
|
>
|
||||||
|
Activate
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => open('https://yaak.app/pricing')}
|
onClick={() => open('https://yaak.app/pricing?ref=app.yaak.desktop')}
|
||||||
rightSlot={<Icon icon="external_link" />}
|
rightSlot={<Icon icon="external_link" />}
|
||||||
|
event="license.purchase"
|
||||||
>
|
>
|
||||||
Purchase
|
Purchase
|
||||||
</Button>
|
</Button>
|
||||||
@@ -98,7 +109,13 @@ export function SettingsLicense() {
|
|||||||
onChange={setKey}
|
onChange={setKey}
|
||||||
placeholder="YK1-XXXXX-XXXXX-XXXXX-XXXXX"
|
placeholder="YK1-XXXXX-XXXXX-XXXXX-XXXXX"
|
||||||
/>
|
/>
|
||||||
<Button type="submit" color="primary" size="sm" isLoading={activate.isPending}>
|
<Button
|
||||||
|
type="submit"
|
||||||
|
color="primary"
|
||||||
|
size="sm"
|
||||||
|
isLoading={activate.isPending}
|
||||||
|
event="license.submit"
|
||||||
|
>
|
||||||
Submit
|
Submit
|
||||||
</Button>
|
</Button>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export function SettingsPlugins() {
|
|||||||
/>
|
/>
|
||||||
<HStack>
|
<HStack>
|
||||||
{directory && (
|
{directory && (
|
||||||
<Button size="xs" type="submit" color="primary" className="ml-auto">
|
<Button size="xs" type="submit" color="primary" className="ml-auto" event="plugin.add">
|
||||||
Add Plugin
|
Add Plugin
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -70,12 +70,14 @@ export function SettingsPlugins() {
|
|||||||
icon="refresh"
|
icon="refresh"
|
||||||
title="Reload plugins"
|
title="Reload plugins"
|
||||||
spin={refreshPlugins.isPending}
|
spin={refreshPlugins.isPending}
|
||||||
|
event="plugin.reload"
|
||||||
onClick={() => refreshPlugins.mutate()}
|
onClick={() => refreshPlugins.mutate()}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
icon="help"
|
icon="help"
|
||||||
title="View documentation"
|
title="View documentation"
|
||||||
|
event="plugin.docs"
|
||||||
onClick={() => open('https://feedback.yaak.app/help/articles/6911763-quick-start')}
|
onClick={() => open('https://feedback.yaak.app/help/articles/6911763-quick-start')}
|
||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
@@ -100,6 +102,7 @@ function PluginInfo({ plugin }: { plugin: Plugin }) {
|
|||||||
icon="trash"
|
icon="trash"
|
||||||
title="Uninstall plugin"
|
title="Uninstall plugin"
|
||||||
className="text-text-subtlest"
|
className="text-text-subtlest"
|
||||||
|
event="plugin.delete"
|
||||||
onClick={() => deletePlugin.mutate()}
|
onClick={() => deletePlugin.mutate()}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export function SettingsProxy() {
|
|||||||
hideLabel
|
hideLabel
|
||||||
size="sm"
|
size="sm"
|
||||||
value={settings.proxy?.type ?? 'automatic'}
|
value={settings.proxy?.type ?? 'automatic'}
|
||||||
|
event="proxy"
|
||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
if (v === 'automatic') {
|
if (v === 'automatic') {
|
||||||
updateSettings.mutate({ proxy: undefined });
|
updateSettings.mutate({ proxy: undefined });
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { HTMLAttributes, ReactNode } from 'react';
|
|||||||
import { forwardRef, useImperativeHandle, useRef } from 'react';
|
import { forwardRef, useImperativeHandle, useRef } from 'react';
|
||||||
import type { HotkeyAction } from '../../hooks/useHotKey';
|
import type { HotkeyAction } from '../../hooks/useHotKey';
|
||||||
import { useFormattedHotkey, useHotKey } from '../../hooks/useHotKey';
|
import { useFormattedHotkey, useHotKey } from '../../hooks/useHotKey';
|
||||||
|
import { trackEvent } from '../../lib/analytics';
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
|
|
||||||
export type ButtonProps = Omit<HTMLAttributes<HTMLButtonElement>, 'color' | 'onChange'> & {
|
export type ButtonProps = Omit<HTMLAttributes<HTMLButtonElement>, 'color' | 'onChange'> & {
|
||||||
@@ -28,6 +29,7 @@ export type ButtonProps = Omit<HTMLAttributes<HTMLButtonElement>, 'color' | 'onC
|
|||||||
leftSlot?: ReactNode;
|
leftSlot?: ReactNode;
|
||||||
rightSlot?: ReactNode;
|
rightSlot?: ReactNode;
|
||||||
hotkeyAction?: HotkeyAction;
|
hotkeyAction?: HotkeyAction;
|
||||||
|
event?: string | { id: string; [attr: string]: number | string };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
||||||
@@ -48,6 +50,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
|||||||
hotkeyAction,
|
hotkeyAction,
|
||||||
title,
|
title,
|
||||||
onClick,
|
onClick,
|
||||||
|
event,
|
||||||
...props
|
...props
|
||||||
}: ButtonProps,
|
}: ButtonProps,
|
||||||
ref,
|
ref,
|
||||||
@@ -107,7 +110,12 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
|||||||
type={type}
|
type={type}
|
||||||
className={classes}
|
className={classes}
|
||||||
disabled={disabled || isLoading}
|
disabled={disabled || isLoading}
|
||||||
onClick={onClick}
|
onClick={(e) => {
|
||||||
|
onClick?.(e);
|
||||||
|
if (event != null) {
|
||||||
|
trackEvent('button', 'click', typeof event === 'string' ? { id: event } : event);
|
||||||
|
}
|
||||||
|
}}
|
||||||
onDoubleClick={(e) => {
|
onDoubleClick={(e) => {
|
||||||
// Kind of a hack? This prevents double-clicks from going through buttons. For example, when
|
// Kind of a hack? This prevents double-clicks from going through buttons. For example, when
|
||||||
// double-clicking the workspace header to toggle window maximization
|
// double-clicking the workspace header to toggle window maximization
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
import { trackEvent } from '../../lib/analytics';
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
import { HStack } from './Stacks';
|
import { HStack } from './Stacks';
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ export interface CheckboxProps {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
inputWrapperClassName?: string;
|
inputWrapperClassName?: string;
|
||||||
hideLabel?: boolean;
|
hideLabel?: boolean;
|
||||||
|
event?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Checkbox({
|
export function Checkbox({
|
||||||
@@ -21,6 +23,7 @@ export function Checkbox({
|
|||||||
disabled,
|
disabled,
|
||||||
title,
|
title,
|
||||||
hideLabel,
|
hideLabel,
|
||||||
|
event,
|
||||||
}: CheckboxProps) {
|
}: CheckboxProps) {
|
||||||
return (
|
return (
|
||||||
<HStack
|
<HStack
|
||||||
@@ -37,7 +40,12 @@ export function Checkbox({
|
|||||||
)}
|
)}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={() => onChange(checked === 'indeterminate' ? true : !checked)}
|
onChange={() => {
|
||||||
|
onChange(checked === 'indeterminate' ? true : !checked);
|
||||||
|
if (event != null) {
|
||||||
|
trackEvent('button', 'click', { id: event, checked: checked ? 'on' : 'off' });
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
<Icon
|
<Icon
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { HTMLAttributes } from 'react';
|
import type { HTMLAttributes } from 'react';
|
||||||
import { Link as RouterLink } from 'react-router-dom';
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
|
import { trackEvent } from '../../lib/analytics';
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
|
|
||||||
interface Props extends HTMLAttributes<HTMLAnchorElement> {
|
interface Props extends HTMLAttributes<HTMLAnchorElement> {
|
||||||
href: string;
|
href: string;
|
||||||
|
event?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Link({ href, children, className, ...other }: Props) {
|
export function Link({ href, children, className, event, ...other }: Props) {
|
||||||
const isExternal = href.match(/^https?:\/\//);
|
const isExternal = href.match(/^https?:\/\//);
|
||||||
|
|
||||||
className = classNames(className, 'relative underline hover:text-violet-600');
|
className = classNames(className, 'relative underline hover:text-violet-600');
|
||||||
@@ -19,6 +21,12 @@ export function Link({ href, children, className, ...other }: Props) {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className={classNames(className, 'pr-4 inline-flex items-center')}
|
className={classNames(className, 'pr-4 inline-flex items-center')}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (event != null) {
|
||||||
|
trackEvent('link', 'click', { id: event });
|
||||||
|
}
|
||||||
|
}}
|
||||||
{...other}
|
{...other}
|
||||||
>
|
>
|
||||||
<span className="underline">{children}</span>
|
<span className="underline">{children}</span>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import classNames from 'classnames';
|
|||||||
import type { CSSProperties, ReactNode } from 'react';
|
import type { CSSProperties, ReactNode } from 'react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useOsInfo } from '../../hooks/useOsInfo';
|
import { useOsInfo } from '../../hooks/useOsInfo';
|
||||||
|
import { trackEvent } from '../../lib/analytics';
|
||||||
import type { ButtonProps } from './Button';
|
import type { ButtonProps } from './Button';
|
||||||
import { Button } from './Button';
|
import { Button } from './Button';
|
||||||
import type { RadioDropdownItem } from './RadioDropdown';
|
import type { RadioDropdownItem } from './RadioDropdown';
|
||||||
@@ -20,6 +21,7 @@ export interface SelectProps<T extends string> {
|
|||||||
onChange: (value: T) => void;
|
onChange: (value: T) => void;
|
||||||
size?: ButtonProps['size'];
|
size?: ButtonProps['size'];
|
||||||
className?: string;
|
className?: string;
|
||||||
|
event?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Select<T extends string>({
|
export function Select<T extends string>({
|
||||||
@@ -33,6 +35,7 @@ export function Select<T extends string>({
|
|||||||
leftSlot,
|
leftSlot,
|
||||||
onChange,
|
onChange,
|
||||||
className,
|
className,
|
||||||
|
event,
|
||||||
size = 'md',
|
size = 'md',
|
||||||
}: SelectProps<T>) {
|
}: SelectProps<T>) {
|
||||||
const osInfo = useOsInfo();
|
const osInfo = useOsInfo();
|
||||||
@@ -40,6 +43,13 @@ export function Select<T extends string>({
|
|||||||
const id = `input-${name}`;
|
const id = `input-${name}`;
|
||||||
const isInvalidSelection = options.find((o) => 'value' in o && o.value === value) == null;
|
const isInvalidSelection = options.find((o) => 'value' in o && o.value === value) == null;
|
||||||
|
|
||||||
|
const handleChange = (value: T) => {
|
||||||
|
onChange?.(value);
|
||||||
|
if (event != null) {
|
||||||
|
trackEvent('select', 'click', { id: event, value });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
@@ -79,7 +89,7 @@ export function Select<T extends string>({
|
|||||||
<select
|
<select
|
||||||
value={value}
|
value={value}
|
||||||
style={selectBackgroundStyles}
|
style={selectBackgroundStyles}
|
||||||
onChange={(e) => onChange(e.target.value as T)}
|
onChange={(e) => handleChange(e.target.value as T)}
|
||||||
onFocus={() => setFocused(true)}
|
onFocus={() => setFocused(true)}
|
||||||
onBlur={() => setFocused(false)}
|
onBlur={() => setFocused(false)}
|
||||||
className={classNames('pr-7 w-full outline-none bg-transparent')}
|
className={classNames('pr-7 w-full outline-none bg-transparent')}
|
||||||
@@ -98,7 +108,7 @@ export function Select<T extends string>({
|
|||||||
) : (
|
) : (
|
||||||
// Use custom "select" component until Tauri can be configured to have select menus not always appear in
|
// Use custom "select" component until Tauri can be configured to have select menus not always appear in
|
||||||
// light mode
|
// light mode
|
||||||
<RadioDropdown value={value} onChange={onChange} items={options}>
|
<RadioDropdown value={value} onChange={handleChange} items={options}>
|
||||||
<Button
|
<Button
|
||||||
className="w-full text-sm font-mono"
|
className="w-full text-sm font-mono"
|
||||||
justify="start"
|
justify="start"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { memo, useEffect, useRef } from 'react';
|
import { memo, useEffect, useRef } from 'react';
|
||||||
|
import { trackEvent } from '../../../lib/analytics';
|
||||||
import { Icon } from '../Icon';
|
import { Icon } from '../Icon';
|
||||||
import type { RadioDropdownProps } from '../RadioDropdown';
|
import type { RadioDropdownProps } from '../RadioDropdown';
|
||||||
import { RadioDropdown } from '../RadioDropdown';
|
import { RadioDropdown } from '../RadioDropdown';
|
||||||
@@ -95,7 +96,14 @@ export function Tabs({
|
|||||||
onChange={t.options.onChange}
|
onChange={t.options.onChange}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={isActive ? undefined : () => onChangeValue(t.value)}
|
onClick={
|
||||||
|
isActive
|
||||||
|
? undefined
|
||||||
|
: () => {
|
||||||
|
trackEvent('tab', 'click', { label, tab: t.value });
|
||||||
|
onChangeValue(t.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
className={btnClassName}
|
className={btnClassName}
|
||||||
>
|
>
|
||||||
{option && 'shortLabel' in option
|
{option && 'shortLabel' in option
|
||||||
@@ -113,7 +121,14 @@ export function Tabs({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={t.value}
|
key={t.value}
|
||||||
onClick={() => onChangeValue(t.value)}
|
onClick={
|
||||||
|
isActive
|
||||||
|
? undefined
|
||||||
|
: () => {
|
||||||
|
trackEvent('tab', 'click', { label, tab: t.value });
|
||||||
|
onChangeValue(t.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
className={btnClassName}
|
className={btnClassName}
|
||||||
>
|
>
|
||||||
{t.label}
|
{t.label}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
import type { SettingsTab } from '../components/Settings/Settings';
|
import { SettingsTab } from '../components/Settings/Settings';
|
||||||
|
import { trackEvent } from '../lib/analytics';
|
||||||
import { invokeCmd } from '../lib/tauri';
|
import { invokeCmd } from '../lib/tauri';
|
||||||
import { useActiveWorkspace } from './useActiveWorkspace';
|
import { useActiveWorkspace } from './useActiveWorkspace';
|
||||||
import { useAppRoutes } from './useAppRoutes';
|
import { useAppRoutes } from './useAppRoutes';
|
||||||
|
|
||||||
export function useOpenSettings(tab?: SettingsTab) {
|
export function useOpenSettings(tab = SettingsTab.General) {
|
||||||
const routes = useAppRoutes();
|
const routes = useAppRoutes();
|
||||||
const workspace = useActiveWorkspace();
|
const workspace = useActiveWorkspace();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
@@ -12,6 +13,7 @@ export function useOpenSettings(tab?: SettingsTab) {
|
|||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
if (workspace == null) return;
|
if (workspace == null) return;
|
||||||
|
|
||||||
|
trackEvent('dialog', 'show', { id: 'settings', tab: `${tab}` });
|
||||||
await invokeCmd('cmd_new_child_window', {
|
await invokeCmd('cmd_new_child_window', {
|
||||||
url: routes.paths.workspaceSettings({ workspaceId: workspace.id, tab }),
|
url: routes.paths.workspaceSettings({ workspaceId: workspace.id, tab }),
|
||||||
label: 'settings',
|
label: 'settings',
|
||||||
|
|||||||
@@ -1,41 +1,9 @@
|
|||||||
|
import type { AnalyticsAction, AnalyticsResource } from '../../src-tauri/bindings/analytics';
|
||||||
import { invokeCmd } from './tauri';
|
import { invokeCmd } from './tauri';
|
||||||
|
|
||||||
export type TrackResource =
|
|
||||||
| 'appearance'
|
|
||||||
| 'app'
|
|
||||||
| 'cookie_jar'
|
|
||||||
| 'dialog'
|
|
||||||
| 'environment'
|
|
||||||
| 'folder'
|
|
||||||
| 'grpc_connection'
|
|
||||||
| 'grpc_event'
|
|
||||||
| 'grpc_request'
|
|
||||||
| 'http_request'
|
|
||||||
| 'http_response'
|
|
||||||
| 'key_value'
|
|
||||||
| 'plugin'
|
|
||||||
| 'setting'
|
|
||||||
| 'sidebar'
|
|
||||||
| 'theme'
|
|
||||||
| 'workspace';
|
|
||||||
|
|
||||||
export type TrackAction =
|
|
||||||
| 'cancel'
|
|
||||||
| 'commit'
|
|
||||||
| 'create'
|
|
||||||
| 'delete'
|
|
||||||
| 'delete_many'
|
|
||||||
| 'duplicate'
|
|
||||||
| 'hide'
|
|
||||||
| 'launch'
|
|
||||||
| 'send'
|
|
||||||
| 'show'
|
|
||||||
| 'toggle'
|
|
||||||
| 'update';
|
|
||||||
|
|
||||||
export function trackEvent(
|
export function trackEvent(
|
||||||
resource: TrackResource,
|
resource: AnalyticsResource,
|
||||||
action: TrackAction,
|
action: AnalyticsAction,
|
||||||
attributes: Record<string, string | number> = {},
|
attributes: Record<string, string | number> = {},
|
||||||
) {
|
) {
|
||||||
invokeCmd('cmd_track_event', {
|
invokeCmd('cmd_track_event', {
|
||||||
|
|||||||
Reference in New Issue
Block a user