From 61ed18553b4ad9b1671305d1408383f7b2b3dc72 Mon Sep 17 00:00:00 2001 From: Sin-Kang Date: Wed, 3 Jun 2026 20:46:56 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20admin-ui=20UX=20pass=20=E2=80=94=20acti?= =?UTF-8?q?on=20tooltips,=20session-expiry=20redirect,=20empty=20states?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three usability fixes raised while operating the console: - **Action tooltips.** Every icon-only action button now shows a hover tooltip (PrimeVue Tooltip directive, mirroring the existing aria-labels) across all list pages + the header — so the row actions are no longer guesswork. - **Session-expiry → login.** The JWT `exp` is now honored: an expired token is treated as logged-out, so the router guard bounces the user to the login screen (with a "session expired" notice) instead of leaving them on pages that silently 403. The axios interceptor also handles 401 and 403-from-an-expired-token; a genuine 403 (valid token, missing permission) is left alone. - **Empty states.** A reusable EmptyState component (icon + message) replaces the bare "no results" line on the Users/Roles/Permissions/Groups/Menus lists. Built green (vue-tsc + vite). Admin-ui only — no backend change. --- src/api/client.ts | 14 +++++++++++--- src/components/EmptyState.vue | 12 ++++++++++++ src/i18n/locales/en.ts | 1 + src/i18n/locales/ko.ts | 1 + src/layout/AppLayout.vue | 4 ++-- src/main.ts | 2 ++ src/router/index.ts | 9 ++++++++- src/stores/auth.ts | 23 ++++++++++++++++++++++- src/views/AuditLogsView.vue | 4 ++-- src/views/DashboardView.vue | 2 +- src/views/GroupsView.vue | 13 +++++++------ src/views/LoginView.vue | 7 +++++++ src/views/MenusView.vue | 11 ++++++----- src/views/PermissionsView.vue | 9 +++++---- src/views/PoliciesView.vue | 4 ++-- src/views/RolesView.vue | 11 ++++++----- src/views/SettingsView.vue | 2 +- src/views/TenantsView.vue | 8 ++++---- src/views/UsersView.vue | 17 +++++++++-------- 19 files changed, 109 insertions(+), 45 deletions(-) create mode 100644 src/components/EmptyState.vue diff --git a/src/api/client.ts b/src/api/client.ts index bb6b9bf..be18326 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -36,10 +36,18 @@ client.interceptors.response.use( // the global handler for those and let the caller deal with it. const url = error?.config?.url ?? '' const isAuthEndpoint = typeof url === 'string' && url.includes('/auth/') - if (error?.response?.status === 401 && !isAuthEndpoint) { + const status = error?.response?.status + if (!isAuthEndpoint && (status === 401 || status === 403)) { const auth = useAuthStore() - auth.clear() - router.replace({ name: 'login' }) + // 401 = unauthenticated. 403 with an expired token = the kit rejects a stale + // session as "forbidden" (the expired JWT falls back to anonymous). Either way + // the session is dead → sign out and bounce to login with an "expired" hint. + // A genuine 403 (valid token, missing permission) is left alone so the calling + // view can surface a "no permission" message instead of logging the user out. + if (status === 401 || auth.isExpired()) { + auth.clear() + router.replace({ name: 'login', query: { expired: '1' } }) + } } return Promise.reject(error) }, diff --git a/src/components/EmptyState.vue b/src/components/EmptyState.vue new file mode 100644 index 0000000..4cb92c7 --- /dev/null +++ b/src/components/EmptyState.vue @@ -0,0 +1,12 @@ + + + diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index d513cc0..8c59960 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -102,6 +102,7 @@ export default { password: 'Password', submit: 'Sign in', failed: 'Login failed', + sessionExpired: 'Your session expired. Please sign in again.', }, changePassword: { title: 'Change your password', diff --git a/src/i18n/locales/ko.ts b/src/i18n/locales/ko.ts index b0ca8e4..f70429f 100644 --- a/src/i18n/locales/ko.ts +++ b/src/i18n/locales/ko.ts @@ -102,6 +102,7 @@ export default { password: '비밀번호', submit: '로그인', failed: '로그인 실패', + sessionExpired: '세션이 만료되었습니다. 다시 로그인해 주세요.', }, changePassword: { title: '비밀번호 변경', diff --git a/src/layout/AppLayout.vue b/src/layout/AppLayout.vue index 9f0295a..8fbaad4 100644 --- a/src/layout/AppLayout.vue +++ b/src/layout/AppLayout.vue @@ -108,7 +108,7 @@ function toggleLocale() { text rounded severity="secondary" - :aria-label="t('app.toggleLocale')" + v-tooltip.top="t('app.toggleLocale')" :aria-label="t('app.toggleLocale')" data-testid="locale-toggle" @click="toggleLocale" > @@ -119,7 +119,7 @@ function toggleLocale() { rounded :icon="ui.theme === 'dark' ? 'pi pi-sun' : 'pi pi-moon'" severity="secondary" - :aria-label="t('app.toggleTheme')" + v-tooltip.top="t('app.toggleTheme')" :aria-label="t('app.toggleTheme')" @click="ui.toggleTheme()" />