feat: Agent registration UX flow updates (#4942)
Co-authored-by: curiouscorrelation <curiouscorrelation@gmail.com> Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
parent
3c535b2ad4
commit
f564b2e34f
7 changed files with 170 additions and 107 deletions
|
|
@ -181,6 +181,20 @@ pub fn run() {
|
|||
.unwrap();
|
||||
};
|
||||
|
||||
let app_handle_ref = app_handle.clone();
|
||||
app_handle.listen("maximize-window", move |_| {
|
||||
tracing::info!("Maximize window event triggered");
|
||||
if let Some(window) = app_handle_ref.get_webview_window("main") {
|
||||
if let Err(e) = window.emit("show-otp-view", ()) {
|
||||
tracing::error!("Failed to emit show-otp-view event: {}", e);
|
||||
}
|
||||
|
||||
if let Err(e) = show_main_window(&app_handle_ref) {
|
||||
tracing::error!("Failed to maximize window: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let app_handle_ref = app_handle.clone();
|
||||
app_handle.listen("registration-received", move |_| {
|
||||
tracing::info!("Registration received event triggered");
|
||||
|
|
|
|||
|
|
@ -23,6 +23,13 @@ pub fn create_tray(app: &AppHandle) -> tauri::Result<()> {
|
|||
true,
|
||||
None::<&str>,
|
||||
)?;
|
||||
let maximize_window = MenuItem::with_id(
|
||||
app,
|
||||
"maximize_window",
|
||||
"Maximize Window",
|
||||
true,
|
||||
None::<&str>,
|
||||
)?;
|
||||
let show_registrations = MenuItem::with_id(
|
||||
app,
|
||||
"show_registrations",
|
||||
|
|
@ -48,9 +55,12 @@ pub fn create_tray(app: &AppHandle) -> tauri::Result<()> {
|
|||
.item(&app_name_item)
|
||||
.item(&app_version_item)
|
||||
.separator()
|
||||
.item(&maximize_window)
|
||||
.separator()
|
||||
.item(&clear_registrations)
|
||||
.item(&show_registrations)
|
||||
.separator()
|
||||
.separator()
|
||||
.item(&quit_i)
|
||||
.build()?;
|
||||
|
||||
|
|
@ -63,7 +73,7 @@ pub fn create_tray(app: &AppHandle) -> tauri::Result<()> {
|
|||
})
|
||||
.icon_as_template(cfg!(target_os = "macos"))
|
||||
.menu(&menu)
|
||||
.menu_on_left_click(true)
|
||||
.show_menu_on_left_click(true)
|
||||
.on_menu_event(move |app, event| match event.id.as_ref() {
|
||||
"quit" => {
|
||||
tracing::info!("Exiting the agent...");
|
||||
|
|
@ -85,6 +95,15 @@ pub fn create_tray(app: &AppHandle) -> tauri::Result<()> {
|
|||
tracing::error!("Failed to show window: {}", e);
|
||||
}
|
||||
}
|
||||
"maximize_window" => {
|
||||
app.emit("maximize-window", ())
|
||||
.unwrap_or_else(|e| {
|
||||
tracing::error!("Failed to emit maximize-window event: {}", e);
|
||||
});
|
||||
if let Err(e) = show_main_window(&app) {
|
||||
tracing::error!("Failed to maximize window: {}", e);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
tracing::warn!("Unhandled menu event: {:?}", event.id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,23 +2,18 @@
|
|||
<div class="h-screen p-5 flex flex-col gap-y-2">
|
||||
<h1 class="font-bold text-lg text-white">{{ pipe(state(), getTitle) }}</h1>
|
||||
|
||||
<template v-if="isOtpView(state())">
|
||||
<div v-if="state().otp" class="flex-grow">
|
||||
<template v-if="O.isSome(state().otp)">
|
||||
<div class="flex-grow">
|
||||
<p class="tracking-wide">
|
||||
An app is trying to register against the Hoppscotch Agent. If this was intentional, copy the given code into
|
||||
the app to complete the registration process. Please hide the window if you did not initiate this request.
|
||||
Do not hide this window until the verification code is entered. The window will hide automatically once done.
|
||||
the app to complete the registration process. Please cancel the registration if you did not initiate this request.
|
||||
The window will hide automatically once registration succeeds. If you minimize this window during registration,
|
||||
you can access it again from the tray by selecting "Maximize Window".
|
||||
</p>
|
||||
<p
|
||||
class="font-bold text-5xl tracking-wider text-center pt-10 text-white"
|
||||
>{{ pipe(state().otp, O.getOrElse(() => "")) }}</p>
|
||||
</div>
|
||||
<div v-else class="text-center pt-10 flex-grow">
|
||||
<p class="tracking-wide">Waiting for registration requests...</p>
|
||||
<p
|
||||
class="text-sm text-gray-400 mt-2"
|
||||
>You can hide this window and access it again from the tray icon.</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
|
|
@ -38,7 +33,27 @@
|
|||
:icon="copyIcon"
|
||||
@click="copyOtp"
|
||||
/>
|
||||
<HoppButtonPrimary label="Hide Window" outline @click="hideWindow" />
|
||||
<div class="flex gap-2">
|
||||
<template v-if="O.isSome(state().otp)">
|
||||
<HoppButtonSecondary
|
||||
label="Cancel Registration"
|
||||
outline
|
||||
@click="getCurrentWindow().close()"
|
||||
/>
|
||||
<HoppButtonPrimary
|
||||
label="Minimize to Tray"
|
||||
outline
|
||||
@click="hideWindow"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<HoppButtonPrimary
|
||||
v-else
|
||||
label="Minimize to Tray"
|
||||
outline
|
||||
@click="hideWindow"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -94,10 +109,9 @@ const appState = ref<AppState>({
|
|||
|
||||
const state = () => appState.value
|
||||
|
||||
const isOtpView = (s: AppState): boolean => s.view === "otp"
|
||||
const getTitle = (s: AppState): string =>
|
||||
s.view === "otp" ? "Agent Registration Request" : "Agent Registrations"
|
||||
const shouldShowCopy = (s: AppState): boolean => isOtpView(s) && O.isSome(s.otp)
|
||||
O.isSome(s.otp) ? "Agent Registration Request" : "Agent Registrations"
|
||||
const shouldShowCopy = (s: AppState): boolean => O.isSome(s.otp)
|
||||
const formatDate = (date: string): string => new Date(date).toLocaleString()
|
||||
|
||||
const getOtp = TE.tryCatch(
|
||||
|
|
@ -166,7 +180,11 @@ onMounted(async () => {
|
|||
await pipe(
|
||||
getOtp,
|
||||
TE.map((otp: string) => {
|
||||
if (otp) appState.value = { ...state(), otp: O.some(otp) }
|
||||
if (otp) {
|
||||
appState.value = { ...state(), otp: O.some(otp) }
|
||||
} else {
|
||||
updateRegistrations();
|
||||
}
|
||||
})
|
||||
)()
|
||||
|
||||
|
|
@ -181,6 +199,18 @@ onMounted(async () => {
|
|||
),
|
||||
listen("authenticated", handleAuthenticated),
|
||||
listen("show-registrations", handleShowRegistrations),
|
||||
listen("show-otp-view", async () => {
|
||||
await pipe(
|
||||
getOtp,
|
||||
TE.map((otp: string) => {
|
||||
if (otp) {
|
||||
appState.value = { ...state(), otp: O.some(otp) };
|
||||
} else {
|
||||
updateRegistrations();
|
||||
}
|
||||
})
|
||||
)();
|
||||
}),
|
||||
])
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,13 @@
|
|||
:icon="copyIcon"
|
||||
@click="copyCode"
|
||||
/>
|
||||
<HoppButtonPrimary label="Hide Window" outline @click="hideWindow" />
|
||||
<div class="flex gap-2">
|
||||
<HoppButtonSecondary
|
||||
label="Cancel Registration"
|
||||
outline
|
||||
@click="getCurrentWindow().close()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -882,14 +882,15 @@
|
|||
"account_email_description": "Your primary email address.",
|
||||
"account_name_description": "This is your display name.",
|
||||
"additional": "Additional Settings",
|
||||
"agent_not_running": "Hoppscotch Agent not detected - click `Retry` to check again.",
|
||||
"agent_not_running": "Hoppscotch Agent not detected. Please check if the Agent is running.",
|
||||
"agent_not_running_short": "Check Agent's status.",
|
||||
"agent_running": "Hoppscotch Agent is live.",
|
||||
"agent_running_short": "Hoppscotch Agent is live.",
|
||||
"agent_reset_registration": "Reset Registration",
|
||||
"agent_discard_registration": "Discard Agent Registration",
|
||||
"agent_registered": "Agent Registered",
|
||||
"agent_registration_successful": "Agent Registered Successfully",
|
||||
"agent_registration_fetch_failed": "Couldn't fetch Agent registration information",
|
||||
"agent_registration_fetch_failed": "Couldn't fetch Agent registration information. Please re-register the Agent.",
|
||||
"agent_registration_already_in_progress": "Agent registration is already in progress. Please complete or cancel the current registration and try again.",
|
||||
"auto_encode_mode": "Auto",
|
||||
"auto_encode_mode_tooltip": "Encode the parameters in the request only if some special characters are present",
|
||||
"background": "Background",
|
||||
|
|
@ -928,6 +929,7 @@
|
|||
"proxy_url": "Proxy URL",
|
||||
"proxy_use_toggle": "Use the proxy middleware to send requests",
|
||||
"read_the": "Read the",
|
||||
"register_agent": "Register Agent",
|
||||
"reset_default": "Use Default Proxy",
|
||||
"short_codes": "Short codes",
|
||||
"short_codes_description": "Short codes which were created by you.",
|
||||
|
|
|
|||
|
|
@ -6,51 +6,16 @@
|
|||
<div
|
||||
v-if="
|
||||
!store.authKey.value &&
|
||||
(!store.isAgentRunning.value || !hasCheckedAgent)
|
||||
store.hasInitiatedRegistration.value &&
|
||||
store.hasCheckedAgent.value
|
||||
"
|
||||
class="flex flex-1 items-center space-x-2"
|
||||
>
|
||||
<div class="relative flex-1 border border-divider rounded p-2">
|
||||
<span>{{ t("settings.agent_not_running_short") }}</span>
|
||||
</div>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.retry')"
|
||||
:icon="IconRefresh"
|
||||
outline
|
||||
class="rounded"
|
||||
@click="handleAgentCheck"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="!store.authKey.value && !hasInitiatedRegistration"
|
||||
class="flex flex-1 items-center space-x-2"
|
||||
>
|
||||
<div
|
||||
class="relative flex-1 border border-divider rounded p-2 text-accent"
|
||||
>
|
||||
<span>{{ t("settings.agent_running") }}</span>
|
||||
</div>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.register')"
|
||||
:icon="IconPlus"
|
||||
outline
|
||||
class="rounded"
|
||||
@click="initiateRegistration"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="!store.authKey.value"
|
||||
class="flex flex-1 items-center space-x-2"
|
||||
>
|
||||
<HoppSmartInput
|
||||
v-model="registrationOTP"
|
||||
v-model="store.registrationOTP.value"
|
||||
:autofocus="false"
|
||||
:placeholder="' '"
|
||||
:disabled="isRegistering"
|
||||
:disabled="store.isRegistering.value"
|
||||
:label="t('settings.enter_otp')"
|
||||
input-styles="input floating-input"
|
||||
class="flex-1"
|
||||
|
|
@ -59,38 +24,50 @@
|
|||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.confirm')"
|
||||
:icon="IconCheck"
|
||||
:loading="isRegistering"
|
||||
:loading="store.isRegistering.value"
|
||||
outline
|
||||
class="rounded"
|
||||
@click="register"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex relative flex-1 items-center space-x-2">
|
||||
<div
|
||||
v-else-if="store.maskedAuthKey.value"
|
||||
class="flex relative flex-1 items-center space-x-2"
|
||||
>
|
||||
<label
|
||||
class="text-secondaryLight text-tiny absolute -top-2 left-2 px-1 bg-primary"
|
||||
class="text-secondaryLight text-tiny absolute -top-2 left-2 px-1"
|
||||
>{{ t("settings.agent_registered") }}</label
|
||||
>
|
||||
<div
|
||||
class="w-full p-2 border border-dividerLight rounded bg-primary text-secondaryDark cursor-text select-all"
|
||||
>
|
||||
{{ maskedAuthKey }}
|
||||
{{ store.maskedAuthKey.value }}
|
||||
</div>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('settings.agent_reset_registration')"
|
||||
:icon="iconClear"
|
||||
:title="t('settings.agent_discard_registration')"
|
||||
:icon="IconClose"
|
||||
outline
|
||||
class="rounded"
|
||||
@click="resetRegistration"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<HoppButtonSecondary
|
||||
:icon="IconPlus"
|
||||
:label="t('settings.register_agent')"
|
||||
outline
|
||||
class="rounded"
|
||||
@click="handleAgentCheck"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from "vue"
|
||||
import { refAutoReset } from "@vueuse/core"
|
||||
import { computed, onMounted } from "vue"
|
||||
import { useService } from "dioc/vue"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
|
|
@ -98,76 +75,77 @@ import { KernelInterceptorAgentStore } from "~/platform/std/kernel-interceptors/
|
|||
import { KernelInterceptorService } from "~/services/kernel-interceptor.service"
|
||||
|
||||
import IconPlus from "~icons/lucide/plus"
|
||||
import IconRotateCCW from "~icons/lucide/rotate-ccw"
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import IconRefresh from "~icons/lucide/refresh-cw"
|
||||
import IconClose from "~icons/lucide/x"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
const store = useService(KernelInterceptorAgentStore)
|
||||
const interceptorService = useService(KernelInterceptorService)
|
||||
const iconClear = refAutoReset<typeof IconRotateCCW | typeof IconCheck>(
|
||||
IconRotateCCW,
|
||||
1000
|
||||
)
|
||||
|
||||
const isSelected = computed(
|
||||
() => interceptorService.current.value?.id === "agent"
|
||||
)
|
||||
|
||||
const hasInitiatedRegistration = ref(false)
|
||||
const maskedAuthKey = ref("")
|
||||
const hasCheckedAgent = ref(false)
|
||||
const registrationOTP = ref(store.authKey.value ? null : "")
|
||||
const isRegistering = ref(false)
|
||||
|
||||
async function handleAgentCheck() {
|
||||
const handleAgentCheck = async () => {
|
||||
try {
|
||||
await store.checkAgentStatus()
|
||||
hasCheckedAgent.value = true
|
||||
store.hasCheckedAgent.value = true
|
||||
if (!store.isAgentRunning.value) {
|
||||
toast.error(t("settings.agent_not_running"))
|
||||
} else {
|
||||
await initiateRegistration()
|
||||
}
|
||||
} catch (e) {
|
||||
hasCheckedAgent.value = false
|
||||
} catch {
|
||||
store.hasCheckedAgent.value = false
|
||||
toast.error(t("settings.agent_check_failed"))
|
||||
}
|
||||
}
|
||||
|
||||
async function initiateRegistration() {
|
||||
const initiateRegistration = async () => {
|
||||
try {
|
||||
await store.initiateRegistration()
|
||||
hasInitiatedRegistration.value = true
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function register() {
|
||||
if (!registrationOTP.value) return
|
||||
isRegistering.value = true
|
||||
try {
|
||||
await store.verifyRegistration(registrationOTP.value)
|
||||
await updateMaskedAuthKey()
|
||||
toast.success(t("settings.agent_registration_successful"))
|
||||
registrationOTP.value = ""
|
||||
} catch (e) {
|
||||
} finally {
|
||||
isRegistering.value = false
|
||||
store.hasInitiatedRegistration.value = true
|
||||
toast.success(t("settings.agent_running"))
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
if (e.message === "There is already an existing registration happening") {
|
||||
toast.error(t("settings.agent_registration_already_in_progress"))
|
||||
} else {
|
||||
toast.error(t("settings.agent_registration_failed"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resetRegistration() {
|
||||
store.authKey.value = null
|
||||
maskedAuthKey.value = ""
|
||||
registrationOTP.value = ""
|
||||
hasInitiatedRegistration.value = false
|
||||
const register = async () => {
|
||||
if (!store.registrationOTP.value) return
|
||||
store.isRegistering.value = true
|
||||
try {
|
||||
await store.verifyRegistration(store.registrationOTP.value)
|
||||
await updateMaskedAuthKey()
|
||||
toast.success(t("settings.agent_registration_successful"))
|
||||
store.registrationOTP.value = ""
|
||||
} catch (e) {
|
||||
} finally {
|
||||
store.isRegistering.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function updateMaskedAuthKey() {
|
||||
const resetRegistration = async () => {
|
||||
await store.resetAuthKey()
|
||||
store.maskedAuthKey.value = ""
|
||||
store.registrationOTP.value = ""
|
||||
store.hasInitiatedRegistration.value = false
|
||||
store.hasCheckedAgent.value = false
|
||||
}
|
||||
|
||||
const updateMaskedAuthKey = async () => {
|
||||
if (!store.authKey.value) return
|
||||
|
||||
try {
|
||||
const registration = await store.fetchRegistrationInfo()
|
||||
maskedAuthKey.value = registration.auth_key_hash
|
||||
store.maskedAuthKey.value = registration.auth_key_hash
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -51,6 +51,13 @@ export class KernelInterceptorAgentStore extends Service {
|
|||
public authKey = ref<string | null>(null)
|
||||
private sharedSecretB16 = ref<string | null>(null)
|
||||
|
||||
// AgentSubtitle component shared variables for unified display across multiple components
|
||||
public hasInitiatedRegistration = ref(false)
|
||||
public maskedAuthKey = ref("")
|
||||
public hasCheckedAgent = ref(false)
|
||||
public registrationOTP = ref(this.authKey.value ? null : "")
|
||||
public isRegistering = ref(false)
|
||||
|
||||
override async onServiceInit() {
|
||||
const initResult = await Store.init()
|
||||
if (E.isLeft(initResult)) {
|
||||
|
|
@ -119,6 +126,12 @@ export class KernelInterceptorAgentStore extends Service {
|
|||
}
|
||||
}
|
||||
|
||||
public async resetAuthKey(): Promise<void> {
|
||||
this.authKey.value = null
|
||||
this.sharedSecretB16.value = null
|
||||
await this.persistStore()
|
||||
}
|
||||
|
||||
private mergeSecurity(
|
||||
...settings: (Required<InputDomainSetting>["security"] | undefined)[]
|
||||
): Required<InputDomainSetting>["security"] | undefined {
|
||||
|
|
@ -195,7 +208,7 @@ export class KernelInterceptorAgentStore extends Service {
|
|||
)
|
||||
|
||||
if (response.data.message !== "Registration received and stored") {
|
||||
throw new Error("Registration failed")
|
||||
throw new Error(response.data.message ?? "Registration failed")
|
||||
}
|
||||
|
||||
return otp
|
||||
|
|
@ -251,6 +264,7 @@ export class KernelInterceptorAgentStore extends Service {
|
|||
if (axios.isAxiosError(error)) {
|
||||
if (error.response?.status === 401) {
|
||||
this.authKey.value = null
|
||||
await this.persistStore()
|
||||
}
|
||||
}
|
||||
throw error
|
||||
|
|
|
|||
Loading…
Reference in a new issue