Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve deploy error user experience #2920

Merged
merged 3 commits into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/playground/src/components/form_validator.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,21 @@ export default {
if (statusMap.value.get(uid) !== status) {
statusMap.value.set(uid, status);
}

const el = serviceMap.value.get(uid)?.$el;
if (status === ValidatorStatus.Valid && el) {
const input =
el instanceof HTMLElement
? el
: el && typeof el === "object" && "value" in el && el.value instanceof HTMLElement
? el.value
: null;

if (input) {
input.classList.remove("weblet-layout-error");
setTimeout(() => input.classList.remove("weblet-layout-error-transition"), 152);
}
}
},

reset() {
Expand Down
220 changes: 126 additions & 94 deletions packages/playground/src/components/networks.vue
Original file line number Diff line number Diff line change
@@ -1,100 +1,107 @@
<template>
<input-tooltip tooltip="Enable the network options to be able access your deployment">
<v-expansion-panels variant="inset" class="mb-4">
<v-expansion-panel expand-icon="mdi-menu-down" collapse-icon="mdi-menu-up" :disabled="$props.disabled">
<template v-slot:title>
<span class="text-h6">Network</span>
<v-chip v-if="error" variant="text">
<v-icon color="warning" icon="mdi-alert-circle" />
</v-chip>
<v-chip v-if="ipv4" variant="outlined" class="ml-2"> IPV4 </v-chip>
<v-chip v-if="ipv6" variant="outlined" class="ml-2"> IPV6 </v-chip>
<v-chip v-if="planetary" variant="outlined" class="ml-2"> Planetary </v-chip>
<v-chip v-if="mycelium" variant="outlined" class="ml-2"> Mycelium </v-chip>
<v-chip v-if="wireguard" variant="outlined" class="ml-2"> Wireguard </v-chip>
</template>
<v-expansion-panel-text>
<input-tooltip
v-if="ipv4 !== null"
inline
tooltip="An Internet Protocol version 4 address that is globally unique and accessible over the internet."
>
<v-switch
hide-details
color="primary"
inset
label="Public IPv4"
:model-value="$props.ipv4"
@update:model-value="$emit('update:ipv4', $event ?? undefined)"
/>
</input-tooltip>
<input-tooltip
v-if="ipv6 !== null"
inline
tooltip="Public IPv6 is the next-generation Internet Protocol that offers an expanded address space to connect a vast number of devices."
>
<v-switch
hide-details
color="primary"
inset
label="Public IPv6"
:modelValue="$props.ipv6"
@update:modelValue="$emit('update:ipv6', $event ?? undefined)"
/>
</input-tooltip>
<input-tooltip
v-if="planetary !== null"
inline
tooltip="The Planetary Network is a distributed network infrastructure that spans across multiple regions and countries, providing global connectivity."
>
<v-switch
hide-details
color="primary"
inset
label="Planetary Network"
:modelValue="$props.planetary"
@update:modelValue="$emit('update:planetary', $event ?? undefined)"
/>
</input-tooltip>
<input-tooltip
v-if="mycelium !== null"
inline
tooltip="Mycelium is an IPv6 overlay network. Each node that joins the overlay network will receive an overlay network IP."
>
<v-switch
hide-details
color="primary"
inset
label="mycelium"
:modelValue="$props.mycelium"
@update:modelValue="$emit('update:mycelium', $event ?? undefined)"
/>
</input-tooltip>
<input-tooltip
v-if="wireguard !== null"
inline
tooltip="Enabling WireGuard Access allows you to establish private, secure, and encrypted connections to your instance."
>
<v-switch
hide-details
color="primary"
inset
label="Add Wireguard Access"
:modelValue="$props.wireguard"
@update:modelValue="$emit('update:wireguard', $event ?? undefined)"
/>
</input-tooltip>
<v-alert v-show="error" class="mb-2" type="warning" variant="tonal">
You must enable at least one of network options.
</v-alert>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</input-tooltip>
<div ref="input">
<input-tooltip tooltip="Enable the network options to be able access your deployment">
<v-expansion-panels variant="inset" class="mb-4">
<v-expansion-panel expand-icon="mdi-menu-down" collapse-icon="mdi-menu-up" :disabled="$props.disabled">
<template v-slot:title>
<span class="text-h6">Network</span>
<v-chip v-if="error" variant="text">
<v-icon color="warning" icon="mdi-alert-circle" />
</v-chip>
<v-chip v-if="ipv4" variant="outlined" class="ml-2"> IPV4 </v-chip>
<v-chip v-if="ipv6" variant="outlined" class="ml-2"> IPV6 </v-chip>
<v-chip v-if="planetary" variant="outlined" class="ml-2"> Planetary </v-chip>
<v-chip v-if="mycelium" variant="outlined" class="ml-2"> Mycelium </v-chip>
<v-chip v-if="wireguard" variant="outlined" class="ml-2"> Wireguard </v-chip>
</template>
<v-expansion-panel-text>
<input-tooltip
v-if="ipv4 !== null"
inline
tooltip="An Internet Protocol version 4 address that is globally unique and accessible over the internet."
>
<v-switch
hide-details
color="primary"
inset
label="Public IPv4"
:model-value="$props.ipv4"
@update:model-value="$emit('update:ipv4', $event ?? undefined)"
/>
</input-tooltip>
<input-tooltip
v-if="ipv6 !== null"
inline
tooltip="Public IPv6 is the next-generation Internet Protocol that offers an expanded address space to connect a vast number of devices."
>
<v-switch
hide-details
color="primary"
inset
label="Public IPv6"
:modelValue="$props.ipv6"
@update:modelValue="$emit('update:ipv6', $event ?? undefined)"
/>
</input-tooltip>
<input-tooltip
v-if="planetary !== null"
inline
tooltip="The Planetary Network is a distributed network infrastructure that spans across multiple regions and countries, providing global connectivity."
>
<v-switch
hide-details
color="primary"
inset
label="Planetary Network"
:modelValue="$props.planetary"
@update:modelValue="$emit('update:planetary', $event ?? undefined)"
/>
</input-tooltip>
<input-tooltip
v-if="mycelium !== null"
inline
tooltip="Mycelium is an IPv6 overlay network. Each node that joins the overlay network will receive an overlay network IP."
>
<v-switch
hide-details
color="primary"
inset
label="mycelium"
:modelValue="$props.mycelium"
@update:modelValue="$emit('update:mycelium', $event ?? undefined)"
/>
</input-tooltip>
<input-tooltip
v-if="wireguard !== null"
inline
tooltip="Enabling WireGuard Access allows you to establish private, secure, and encrypted connections to your instance."
>
<v-switch
hide-details
color="primary"
inset
label="Add Wireguard Access"
:modelValue="$props.wireguard"
@update:modelValue="$emit('update:wireguard', $event ?? undefined)"
/>
</input-tooltip>
<v-alert v-show="error" class="mb-2" type="warning" variant="tonal">
You must enable at least one of network options.
</v-alert>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</input-tooltip>
</div>
</template>

<script lang="ts">
import { computed, watch } from "vue";
import { noop } from "lodash";
import { computed, getCurrentInstance, onMounted, onUnmounted, ref, watch } from "vue";

import { useForm, ValidatorStatus } from "@/hooks/form_validator";
import type { InputValidatorService } from "@/hooks/input_validator";

export default {
name: "Network",
props: {
Expand Down Expand Up @@ -131,7 +138,9 @@ export default {
"update:mycelium": (value?: boolean) => value,
"update:wireguard": (value?: boolean) => value,
},
setup(props, { expose, emit }) {
setup(props, { expose }) {
const input = ref();

if (
props.ipv4 === null &&
props.ipv6 === null &&
Expand All @@ -148,8 +157,31 @@ export default {
error,
});

/* Adapter to work with old code validation */
const { uid } = getCurrentInstance() as { uid: number };
const form = useForm();

const fakeService: InputValidatorService = {
validate: () => Promise.resolve(true),
setStatus: noop,
reset: noop,
status: ValidatorStatus.Valid,
error: null,
$el: input,
highlightOnError: true,
};

onMounted(() => form?.register(uid.toString(), fakeService));
onUnmounted(() => form?.unregister(uid.toString()));

watch(error, invalid => {
fakeService.status = invalid ? ValidatorStatus.Invalid : ValidatorStatus.Valid;
form?.updateStatus(uid.toString(), fakeService.status);
});

return {
error,
input,
};
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ export default {
status: ValidatorStatus.Init,
error: null,
$el: input,
highlightOnError: true,
};

onMounted(() => form?.register(uid.toString(), fakeService));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ export default defineComponent({
status: ValidatorStatus.Init,
error: null,
$el: inputElement,
highlightOnError: true,
};

onMounted(() => form?.register(`${uid}`, fakeService));
Expand Down
49 changes: 41 additions & 8 deletions packages/playground/src/components/weblet_layout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@
<template v-else>
<v-alert variant="tonal" type="info" v-show="!profileManager.profile"> Please connect your wallet </v-alert>

<v-alert variant="tonal" v-show="profileManager.profile && status" :type="alertType">
{{ message }}
</v-alert>
<div ref="msgAlert">
<v-alert variant="tonal" v-show="profileManager.profile && status" :type="alertType">
{{ message }}
</v-alert>
</div>

<div v-show="profileManager.profile && !status">
<slot v-if="profileManager.profile" />
Expand Down Expand Up @@ -158,6 +160,7 @@ const status = ref<WebletStatus>();
const message = ref<string>();
const gridStore = useGrid();
const grid = gridStore.client as GridClient;
const msgAlert = ref<HTMLElement>();

function onLogMessage(msg: string) {
if (typeof msg === "string") {
Expand Down Expand Up @@ -196,7 +199,7 @@ provideService({
function validateBeforeDeploy(fn: () => void) {
const forms = __forms;

let errorInput: [number, any] | null = null;
let errorInput: [number, any, boolean] | null = null;

out: for (let i = 0; i < forms.length; i++) {
const form = forms[i];
Expand All @@ -205,20 +208,20 @@ function validateBeforeDeploy(fn: () => void) {
for (const input of inputs) {
const status = typeof input.status === "string" ? input.status : (input.status as any)?.value;
if (status === ValidatorStatus.Invalid) {
errorInput = [i, input.$el];
errorInput = [i, input.$el, input.highlightOnError || false];
break out;
}

const valid = status === ValidatorStatus.Valid || (status === ValidatorStatus.Init && form.validOnInit);

if ((!status || !valid) && !errorInput) {
errorInput = [i, input.$el];
errorInput = [i, input.$el, input.highlightOnError || false];
}
}
}

if (errorInput) {
const [tab, __input] = errorInput;
const [tab, __input, highlightOnError] = errorInput;

const input =
__input && typeof __input === "object" && "value" in __input && __input.value instanceof HTMLElement
Expand All @@ -240,13 +243,32 @@ function validateBeforeDeploy(fn: () => void) {
return;
}

document.addEventListener("scrollend", () => _input.focus(), { once: true });
document.addEventListener("scrollend", _improveUx, { once: true });
_input.scrollIntoView({ behavior: "smooth", block: "center" });

async function _improveUx() {
if (!(_input instanceof HTMLElement)) return;

if (_input instanceof HTMLInputElement || _input instanceof HTMLTextAreaElement) {
// use `requestAnimationFrame` to avoid browser possible lagging
requestAnimationFrame(() => _input.focus());
requestAnimationFrame(() => _input.blur());
requestAnimationFrame(() => _input.focus());
}

if (input instanceof HTMLElement && highlightOnError) {
input.classList.add("weblet-layout-error-transition");
requestAnimationFrame(() => {
input.classList.add("weblet-layout-error");
});
}
}
}, 250);

return;
}

msgAlert.value?.scrollIntoView({ behavior: "smooth", block: "center" });
return fn();
}

Expand Down Expand Up @@ -449,4 +471,15 @@ export default {
margin-right: 5px;
max-height: 24px;
}

.weblet-layout-error-transition {
will-change: padding;
transition: padding 0.15s ease-in-out;
}

.weblet-layout-error {
border: thin solid rgba(var(--v-theme-error), 1) !important;
padding: 8px !important;
margin-bottom: 8px !important;
}
</style>
1 change: 1 addition & 0 deletions packages/playground/src/hooks/input_validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface InputValidatorService {
status: ValidatorStatus;
error: string | null;
$el?: Ref<HTMLElement | null | undefined> | null;
highlightOnError?: boolean;
}

export function useInputRef(isArray: true): Ref<InputValidatorService[]>;
Expand Down
Loading