feat: introducing user guidance and error management helpers in admin dashboard (#4548)

This commit is contained in:
Joel Jacob Stephen
2024-11-22 10:52:34 -06:00
committed by GitHub
parent dad15133f4
commit 73f3e54c00
12 changed files with 209 additions and 106 deletions

View File

@@ -96,7 +96,7 @@
"last_used_on": "Last used on",
"no_expiration": "No expiration",
"no_expiration_verbose": "This token will never expire!",
"section_description": "Manage your Hoppscotch users through APIs with Infra tokens",
"section_description": "Manage your Hoppscotch users through APIs with Infra tokens.",
"section_title": "Infra Tokens",
"tab_title": "Infra Tokens",
"token_expires_on": "This token will expire on",
@@ -247,6 +247,11 @@
"users_to_admin_success": "Selected users are elevated to admin status!!",
"users_to_admin_failure": "Failed to elevate selected users to admin status!!"
},
"support": {
"description": "Get help from the Hoppscotch community",
"documentation": "Documentation",
"more_info": "More Info"
},
"teams": {
"add_member": "Add Member",
"add_members": "Add Members",
@@ -325,6 +330,7 @@
"invalid_user": "Invalid User",
"invite_load_list_error": "Unable to Load Invited Users List",
"invite_user": "Invite User",
"invite_users_description": "Invite your team members to join Hoppscotch.",
"invited_by": "Invited By",
"invited_on": "Invited On",
"invited_users": "Invited Users",
@@ -341,6 +347,7 @@
"not_available": "Not Available",
"not_found": "User not found",
"pending_invites": "Pending Invites",
"pending_invites_description": "Manage and track pending user invitations with clear status and actions.",
"remove_admin_privilege": "Remove Admin Privilege",
"remove_admin_status": "Remove Admin Status",
"rename": "Rename",

View File

@@ -14,6 +14,7 @@ declare module 'vue' {
AppSidebar: typeof import('./components/app/Sidebar.vue')['default']
AppToast: typeof import('./components/app/Toast.vue')['default']
DashboardMetricsCard: typeof import('./components/dashboard/MetricsCard.vue')['default']
FallbackComponent: typeof import('./components/FallbackComponent.vue')['default']
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
@@ -34,6 +35,7 @@ declare module 'vue' {
HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs']
HoppSmartToggle: typeof import('@hoppscotch/ui')['HoppSmartToggle']
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default']
IconLucideCheck: typeof import('~icons/lucide/check')['default']
IconLucideChevronDown: typeof import('~icons/lucide/chevron-down')['default']
IconLucideHelpCircle: typeof import('~icons/lucide/help-circle')['default']

View File

@@ -24,6 +24,16 @@
</div>
<div class="flex items-center">
<div class="inline-flex items-center mr-5">
<HoppButtonSecondary
to="https://docs.hoppscotch.io/documentation"
blank
v-tippy="{ theme: 'tooltip' }"
:title="t('support.documentation')"
:icon="IconHelpCircle"
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
/>
</div>
<div v-if="currentUser" class="relative">
<tippy
interactive
@@ -69,6 +79,7 @@ import { useSidebar } from '~/composables/useSidebar';
import { auth } from '~/helpers/auth';
import IconMenu from '~icons/lucide/menu';
import IconSidebarOpen from '~icons/lucide/sidebar-open';
import IconHelpCircle from '~icons/lucide/help-circle';
import IconSidebarClose from '~icons/lucide/sidebar-close';
import { useI18n } from '~/composables/i18n';

View File

@@ -17,13 +17,21 @@
v-for="provider in workingConfigs.providers"
class="space-y-4 py-4"
>
<div class="flex items-center">
<div class="flex justify-between">
<HoppSmartToggle
:on="provider.enabled"
@change="provider.enabled = !provider.enabled"
>
{{ capitalize(provider.name) }}
</HoppSmartToggle>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip', allowHTML: true }"
to="https://docs.hoppscotch.io/documentation/self-host/community-edition/prerequisites#oauth"
blank
:title="t('support.documentation')"
:icon="IconCircleHelp"
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
/>
</div>
<div v-if="provider.enabled" class="ml-12">
@@ -67,6 +75,7 @@ import { useVModel } from '@vueuse/core';
import { reactive } from 'vue';
import { useI18n } from '~/composables/i18n';
import { ServerConfigs, SsoAuthProviders } from '~/helpers/configs';
import IconCircleHelp from '~icons/lucide/circle-help';
import IconEye from '~icons/lucide/eye';
import IconEyeOff from '~icons/lucide/eye-off';

View File

@@ -13,12 +13,22 @@
</h4>
<div class="flex items-center space-y-4 py-4">
<HoppSmartToggle
:on="dataSharingConfigs.enabled"
@change="dataSharingConfigs.enabled = !dataSharingConfigs.enabled"
>
{{ t('configs.data_sharing.toggle_description') }}
</HoppSmartToggle>
<div class="flex justify-between w-full">
<HoppSmartToggle
:on="dataSharingConfigs.enabled"
@change="dataSharingConfigs.enabled = !dataSharingConfigs.enabled"
>
{{ t('configs.data_sharing.toggle_description') }}
</HoppSmartToggle>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip', allowHTML: true }"
blank
to="https://docs.hoppscotch.io/documentation/self-host/community-edition/telemetry"
:title="t('support.documentation')"
:icon="IconHelpCircle"
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
/>
</div>
</div>
<HoppButtonSecondary
@@ -30,6 +40,9 @@
blank
class="w-min my-2"
/>
<p class="my-1 text-secondaryLight">
{{ t('configs.data_sharing.description') }}
</p>
</div>
</div>
</template>
@@ -40,6 +53,7 @@ import { computed } from 'vue';
import { useI18n } from '~/composables/i18n';
import { ServerConfigs } from '~/helpers/configs';
import IconShieldQuestion from '~icons/lucide/shield-question';
import IconHelpCircle from '~icons/lucide/help-circle';
const t = useI18n();

View File

@@ -15,12 +15,22 @@
<div class="space-y-4 py-4">
<div class="flex items-center">
<HoppSmartToggle
:on="smtpConfigs.enabled"
@change="smtpConfigs.enabled = !smtpConfigs.enabled"
>
{{ t('configs.mail_configs.enable_smtp') }}
</HoppSmartToggle>
<div class="flex justify-between w-full">
<HoppSmartToggle
:on="smtpConfigs.enabled"
@change="smtpConfigs.enabled = !smtpConfigs.enabled"
>
{{ t('configs.mail_configs.enable_smtp') }}
</HoppSmartToggle>
<HoppButtonSecondary
blank
v-tippy="{ theme: 'tooltip', allowHTML: true }"
to="https://docs.hoppscotch.io/documentation/self-host/community-edition/prerequisites#email-delivery"
:title="t('support.documentation')"
:icon="IconHelpCircle"
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
/>
</div>
</div>
<div v-if="smtpConfigs.enabled" class="ml-12">
@@ -94,6 +104,7 @@ import { useI18n } from '~/composables/i18n';
import { ServerConfigs } from '~/helpers/configs';
import IconEye from '~icons/lucide/eye';
import IconEyeOff from '~icons/lucide/eye-off';
import IconHelpCircle from '~icons/lucide/help-circle';
const t = useI18n();

View File

@@ -5,9 +5,18 @@
{{ t('infra_tokens.section_title') }}
</h4>
<p class="text-secondaryLight">
{{ t('infra_tokens.section_description') }}
</p>
<div class="flex">
<p class="text-secondaryLight">
{{ t('infra_tokens.section_description') }}
</p>
<HoppSmartAnchor
blank
to="https://docs.hoppscotch.io/documentation/self-host/community-edition/admin-dashboard#infratokens"
:label="t('support.more_info')"
class="underline ml-1"
/>
<icon-lucide-arrow-up-right class="underline w-4 h-4" />
</div>
</div>
<HoppButtonSecondary

View File

@@ -13,18 +13,34 @@
/>
</template>
<template #footer>
<span class="flex space-x-2">
<HoppButtonPrimary
:label="t('users.add_user')"
@click="emit('send-invite', email)"
/>
<HoppButtonSecondary
:label="t('users.cancel')"
outline
filled
@click="hideModal"
/>
</span>
<div class="w-full">
<p class="text-secondaryLight mb-5 text-center">
{{ t('users.invite_users_description') }}
</p>
<div class="flex justify-between">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip', allowHTML: true }"
to="https://docs.hoppscotch.io/documentation"
blank
:title="t('support.documentation')"
:icon="IconCircleHelp"
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
/>
<span class="flex space-x-2">
<HoppButtonPrimary
:label="t('users.add_user')"
@click="emit('send-invite', email)"
/>
<HoppButtonSecondary
:label="t('users.cancel')"
outline
filled
@click="hideModal"
/>
</span>
</div>
</div>
</template>
</HoppSmartModal>
</template>
@@ -32,6 +48,7 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useI18n } from '~/composables/i18n';
import IconCircleHelp from '~icons/lucide/circle-help';
const t = useI18n();

View File

@@ -1,7 +1,15 @@
/*
* Type used to send error data to the Fallback catch-all component
*/
export type ErrorPageData = {
message: string;
statusCode?: number;
};
/* No cookies were found in the auth request
* (AuthService)
*/
export const COOKIES_NOT_FOUND = 'auth/cookies_not_found' as const;
export const COOKIES_NOT_FOUND = '[GraphQL] auth/cookies_not_found' as const;
export const UNAUTHORIZED = 'Unauthorized' as const;

View File

@@ -1,65 +1,74 @@
import { createApp } from 'vue';
import urql, { createClient, cacheExchange, fetchExchange } from '@urql/vue';
import { authExchange } from '@urql/exchange-auth';
import urql, { cacheExchange, createClient, fetchExchange } from '@urql/vue';
import { createApp, h } from 'vue';
import App from './App.vue';
import ErrorComponent from './pages/_.vue';
// STYLES
import '@hoppscotch/ui/style.css';
import '../assets/scss/styles.scss';
import '../assets/scss/tailwind.scss';
import '@fontsource-variable/inter';
import '@fontsource-variable/material-symbols-rounded';
import '@fontsource-variable/roboto-mono';
import '@hoppscotch/ui/style.css';
import '../assets/scss/styles.scss';
import '../assets/scss/tailwind.scss';
// END STYLES
import { HOPP_MODULES } from './modules';
import { auth } from './helpers/auth';
import { pipe } from 'fp-ts/function';
import * as O from 'fp-ts/Option';
import { auth } from './helpers/auth';
import { GRAPHQL_UNAUTHORIZED } from './helpers/errors';
import { HOPP_MODULES } from './modules';
// Top-level await is not available in our targets
(async () => {
const app = createApp(App).use(
urql,
createClient({
try {
// Create URQL client
const urqlClient = createClient({
url: import.meta.env.VITE_BACKEND_GQL_URL,
requestPolicy: 'network-only',
fetchOptions: () => {
return {
credentials: 'include',
};
},
fetchOptions: () => ({
credentials: 'include',
}),
exchanges: [
cacheExchange,
authExchange(async () => {
return {
addAuthToOperation(operation) {
return operation;
},
async refreshAuth() {
pipe(
await auth.performAuthRefresh(),
O.getOrElseW(() => auth.signOutUser(true))
);
},
didAuthError(error, _operation) {
return error.message === GRAPHQL_UNAUTHORIZED;
},
};
}),
authExchange(async () => ({
addAuthToOperation(operation) {
return operation;
},
async refreshAuth() {
pipe(
await auth.performAuthRefresh(),
O.getOrElseW(() => auth.signOutUser(true))
);
},
didAuthError(error, _operation) {
return error.message === GRAPHQL_UNAUTHORIZED;
},
})),
fetchExchange,
],
})
);
});
// Initialize auth
await auth.performAuthInit();
// Initialize auth
await auth.performAuthInit();
// Initialize modules
HOPP_MODULES.forEach((mod) => mod.onVueAppInit?.(app));
const app = createApp({
render: () => h(App),
}).use(urql, urqlClient);
app.mount('#app');
// Initialize modules
HOPP_MODULES.forEach((mod) => mod.onVueAppInit?.(app));
app.mount('#app');
} catch (error) {
// Mount the fallback component in case of an error
createApp({
render: () =>
h(ErrorComponent, {
error: {
message:
'Failed to connect to the backend server, make sure the backend is setup correctly',
},
}),
}).mount('#app');
}
})();

View File

@@ -1,41 +1,44 @@
<!-- The Catch-All Page -->
<!-- Reserved for Critical Errors and 404 ONLY -->
<!-- The Fallback Catch-All Page -->
<template>
<div
class="flex flex-col items-center justify-center"
:class="{ 'min-h-screen': statusCode !== 404 }"
class="flex flex-col items-center h-screen"
:class="{ 'min-h-screen': props.error?.statusCode !== 404 }"
>
<img
:src="imgUrl"
loading="lazy"
class="inline-flex flex-col object-contain object-center mb-2 h-46 w-46"
:alt="message"
/>
<h1 class="mb-2 text-4xl heading">
{{ statusCode }}
</h1>
<p class="mb-4 text-secondaryLight">{{ message }}</p>
<p class="mt-4 space-x-2">
<HoppButtonSecondary to="/" :icon="IconHome" filled label="Home" />
<HoppButtonSecondary
:icon="IconRefreshCW"
label="Reload"
filled
@click="reloadApplication"
<div class="flex flex-col items-center justify-center h-full">
<img
:src="imgUrl"
loading="lazy"
class="inline-flex flex-col object-contain object-center mb-2 h-46 w-46"
:alt="message"
/>
</p>
<h1 v-if="props.error?.statusCode" class="mb-2 text-4xl heading">
{{ props.error.statusCode }}
</h1>
<p class="mb-4 text-lg text-secondaryDark">{{ message }}</p>
<p class="mt-4 space-x-2">
<HoppButtonSecondary
to="https://docs.hoppscotch.io/documentation"
:icon="IconTextSearch"
filled
blank
label="Documentation"
/>
<HoppButtonSecondary
:icon="IconRefreshCW"
label="Reload"
filled
@click="reloadApplication"
/>
</p>
</div>
</div>
</template>
<script setup lang="ts">
import IconRefreshCW from '~icons/lucide/refresh-cw';
import IconHome from '~icons/lucide/home';
import { PropType, computed } from 'vue';
export type ErrorPageData = {
message: string;
statusCode: number;
};
import { ErrorPageData } from '~/helpers/errors';
import IconRefreshCW from '~icons/lucide/refresh-cw';
import IconTextSearch from '~icons/lucide/text-search';
const props = defineProps({
error: {
@@ -44,9 +47,7 @@ const props = defineProps({
},
});
const imgUrl = `${import.meta.env.BASE_URL}images/youre_lost.svg`
const statusCode = computed(() => props.error?.statusCode ?? 404);
const imgUrl = `${import.meta.env.BASE_URL}images/youre_lost.svg`;
const message = computed(
() => props.error?.message ?? 'The page you are looking for does not exist.'

View File

@@ -7,9 +7,14 @@
</div>
<div class="flex justify-between items-center py-6">
<h3 class="text-lg font-bold text-accentContrast">
{{ t('users.pending_invites') }}
</h3>
<div>
<h3 class="text-lg font-bold text-accentContrast">
{{ t('users.pending_invites') }}
</h3>
<p class="my-1 text-secondaryLight">
{{ t('users.pending_invites_description') }}
</p>
</div>
<HoppButtonSecondary
v-if="pendingInvites?.length"