Skip to content

Commit 5686e18

Browse files
committed
refactor(ui): extract layout app bars into AppBarContent
Replace inline v-app-bar implementations with AppBarContent in admin and main UI layouts. This centralizes menu toggle, support actions, and slot structure while delegating user-related actions to UserMenu. Tests were updated to assert AppBarContent flags and snapshot output.
1 parent 8c35efa commit 5686e18

File tree

7 files changed

+297
-199
lines changed

7 files changed

+297
-199
lines changed

ui/admin/src/layouts/AppLayout.vue

Lines changed: 31 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -82,73 +82,28 @@
8282
</template>
8383
</v-list>
8484
</v-navigation-drawer>
85-
<v-app-bar
85+
<AppBarContent
8686
:theme
87-
class="bg-v-theme-surface border-b-thin"
88-
data-test="app-bar"
89-
flat
90-
floating
87+
show-menu-toggle
88+
show-support
89+
@toggle-menu="drawer = !drawer"
90+
@support-click="openShellhubHelp"
9191
>
92-
<v-app-bar-nav-icon
93-
class="hidden-lg-and-up"
94-
aria-label="Toggle Menu"
95-
@click.stop="drawer = !drawer"
96-
/>
97-
98-
<Namespace :is-admin-context="true" />
99-
100-
<v-spacer />
101-
102-
<v-menu anchor="bottom">
103-
<template #activator="{ props }">
104-
<v-chip
105-
color="primary"
106-
v-bind="props"
107-
class="mr-8"
108-
>
109-
<UserIcon
110-
size="1.5rem"
111-
:email="currentUser"
112-
class="mr-2"
113-
data-test="user-icon"
114-
/>
115-
{{ currentUser || "ADMIN" }}
116-
<v-icon
117-
right
118-
icon="mdi-chevron-down"
119-
/>
120-
</v-chip>
121-
</template>
122-
<v-list class="bg-v-theme-surface">
123-
<v-list-item
124-
v-for="item in menu"
125-
:key="item.title"
126-
:value="item"
127-
:data-test="item.title"
128-
@click="triggerClick(item)"
129-
>
130-
<div class="d-flex align-center">
131-
<div><v-icon :icon="item.icon" /></div>
132-
<v-list-item-title>{{ item.title }}</v-list-item-title>
133-
</div>
134-
</v-list-item>
135-
136-
<v-divider />
137-
138-
<v-list-item>
139-
<v-switch
140-
label="Dark Mode"
141-
:model-value="isDarkMode"
142-
data-test="dark-mode-switch"
143-
color="primary"
144-
inset
145-
hide-details
146-
@change="toggleDarkMode"
147-
/>
148-
</v-list-item>
149-
</v-list>
150-
</v-menu>
151-
</v-app-bar>
92+
<template #left>
93+
<Namespace :is-admin-context="true" />
94+
</template>
95+
96+
<template #right>
97+
<UserMenu
98+
:user-email="currentUser"
99+
:display-name="displayName"
100+
:menu-items="menu"
101+
:is-dark-mode="isDarkMode"
102+
@select="handleUserMenuSelect"
103+
@toggle-dark-mode="toggleDarkMode"
104+
/>
105+
</template>
106+
</AppBarContent>
152107

153108
<Snackbar />
154109

@@ -189,7 +144,8 @@ import useLayoutStore from "@/store/modules/layout";
189144
import useAuthStore from "@admin/store/modules/auth";
190145
import useSpinnerStore from "@/store/modules/spinner";
191146
import Snackbar from "@/components/Snackbar/Snackbar.vue";
192-
import UserIcon from "@/components/User/UserIcon.vue";
147+
import AppBarContent from "@/components/AppBar/AppBarContent.vue";
148+
import UserMenu from "@/components/AppBar/UserMenu.vue";
193149
import Namespace from "@/components/Namespace/Namespace.vue";
194150
import Logo from "@/assets/logo-inverted.png";
195151
import { createNewAdminClient } from "@/api/http";
@@ -227,6 +183,7 @@ const expiredLicense = computed(() => licenseStore.isExpired);
227183
228184
const hasSpinner = computed(() => spinnerStore.status);
229185
const currentUser = computed(() => authStore.currentUser);
186+
const displayName = computed(() => currentUser.value || "ADMIN");
230187
const currentRoute = computed(() => router.currentRoute);
231188
const theme = computed(() => layoutStore.theme);
232189
const isDarkMode = ref(theme.value === "dark");
@@ -258,11 +215,19 @@ const triggerClick = async (item: MenuItem) => {
258215
}
259216
};
260217
218+
const handleUserMenuSelect = (item: unknown) => {
219+
void triggerClick(item as MenuItem);
220+
};
221+
261222
const toggleDarkMode = () => {
262223
isDarkMode.value = !isDarkMode.value;
263224
layoutStore.setTheme(isDarkMode.value ? "dark" : "light");
264225
};
265226
227+
const openShellhubHelp = () => {
228+
window.open("https://github.com/shellhub-io/shellhub/issues/new/choose", "_blank");
229+
};
230+
266231
const items = reactive([
267232
{
268233
icon: "mdi-view-dashboard",

ui/admin/tests/unit/layouts/AppLayout/__snapshots__/index.spec.ts.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
exports[`AppLayout > Renders the component 1`] = `
44
"<v-navigation-drawer-stub data-v-b779e8e7="" class="bg-v-theme-surface" disableresizewatcher="false" disableroutewatcher="false" expandonhover="true" floating="false" modelvalue="true" permanent="false" railwidth="56" scrim="true" temporary="false" persistent="false" touchless="false" width="256" location="start" sticky="false" border="false" order="0" absolute="false" tile="false" retainfocus="false" capturefocus="false" tag="nav"></v-navigation-drawer-stub>
5-
<v-app-bar-stub data-v-b779e8e7="" theme="dark" class="bg-v-theme-surface border-b-thin" modelvalue="true" location="top" absolute="false" collapse="false" collapseposition="start" density="default" extensionheight="48" flat="true" floating="true" height="64" border="false" tile="false" tag="header" order="0" scrollthreshold="300" data-test="app-bar"></v-app-bar-stub>
5+
<app-bar-content-stub data-v-b779e8e7="" showmenutoggle="true" showsupport="true" theme="dark"></app-bar-content-stub>
66
<snackbar-stub data-v-b779e8e7=""></snackbar-stub>
77
<v-main-stub data-v-b779e8e7="" scrollable="false" tag="main" data-test="main"></v-main-stub>
88
<v-overlay-stub data-v-b779e8e7="" class="align-center justify-center w-100 h-100" _disableglobalstack="false" absolute="false" attach="false" closeonback="true" contained="true" disabled="false" noclickanimation="false" modelvalue="false" persistent="false" scrim="false" zindex="2000" activatorprops="[object Object]" openonhover="false" closeoncontentclick="false" eager="false" locationstrategy="static" location="bottom" origin="auto" sticktotarget="false" viewportmargin="12" scrollstrategy="block" retainfocus="false" capturefocus="false" transition="fade-transition" data-test="overlay"></v-overlay-stub>"

ui/admin/tests/unit/layouts/AppLayout/index.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,10 @@ describe("AppLayout", () => {
3030
const vuetify = createVuetify();
3131
const wrapper = shallowMount(AppLayout, { global: { plugins: [vuetify, routes, SnackbarPlugin] } });
3232
it("Renders the component", () => { expect(wrapper.html()).toMatchSnapshot(); });
33+
it("Passes AppBarContent flags", () => {
34+
const appBarContent = wrapper.findComponent({ name: "AppBarContent" });
35+
expect(appBarContent.exists()).toBe(true);
36+
expect(appBarContent.props("showMenuToggle")).toBe(true);
37+
expect(appBarContent.props("showSupport")).toBe(true);
38+
});
3339
});

ui/src/components/AppBar/AppBar.vue

Lines changed: 24 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,12 @@
11
<template>
22
<PaywallChat v-model="chatSupportPaywall" />
3-
<v-app-bar
4-
flat
5-
floating
6-
class="bg-v-theme-surface border-b-thin"
7-
data-test="app-bar"
3+
<AppBarContent
4+
show-menu-toggle
5+
show-support
6+
@toggle-menu="showNavigationDrawer = !showNavigationDrawer"
7+
@support-click="openShellhubHelp()"
88
>
9-
<v-app-bar-nav-icon
10-
class="hidden-lg-and-up"
11-
aria-label="Toggle Menu"
12-
data-test="menu-toggle"
13-
@click.stop="showNavigationDrawer = !showNavigationDrawer"
14-
/>
15-
16-
<div class="d-flex align-center hidden-md-and-down">
9+
<template #left>
1710
<Namespace data-test="namespace-selector" />
1811

1912
<v-breadcrumbs
@@ -36,29 +29,9 @@
3629
/>
3730
</template>
3831
</v-breadcrumbs>
39-
</div>
40-
41-
<v-spacer />
42-
43-
<div class="d-flex align-center ga-4 mr-4">
44-
<v-tooltip
45-
location="bottom"
46-
class="text-center"
47-
>
48-
<template #activator="{ props }">
49-
<v-btn
50-
v-bind="props"
51-
size="medium"
52-
color="primary"
53-
aria-label="community-help-icon"
54-
icon="mdi-help-circle"
55-
data-test="support-btn"
56-
@click="openShellhubHelp()"
57-
/>
58-
</template>
59-
<span>Need assistance? Click here for support.</span>
60-
</v-tooltip>
32+
</template>
6133

34+
<template #right>
6235
<DevicesDropdown
6336
v-if="hasNamespaces"
6437
v-model="showDevicesDrawer"
@@ -71,102 +44,16 @@
7144
@update:model-value="showDevicesDrawer = false"
7245
/>
7346

74-
<v-menu
75-
scrim
76-
location="bottom end"
77-
:offset="4"
78-
>
79-
<template #activator="{ props }">
80-
<v-btn
81-
v-bind="props"
82-
size="medium"
83-
color="primary"
84-
icon
85-
data-test="user-menu-btn"
86-
>
87-
<UserIcon
88-
size="1.5rem"
89-
:email="userEmail"
90-
data-test="user-icon"
91-
/>
92-
</v-btn>
93-
</template>
94-
95-
<v-card
96-
:width="$vuetify.display.thresholds.sm / 2"
97-
border
98-
>
99-
<v-list class="bg-v-theme-surface pa-0">
100-
<!-- User Profile Header -->
101-
<div class="pa-6 text-center">
102-
<UserIcon
103-
size="4rem"
104-
:email="userEmail"
105-
class="mb-4"
106-
data-test="user-icon-large"
107-
/>
108-
<div class="text-h6 font-weight-medium mb-1">
109-
{{ currentUser || userEmail }}
110-
</div>
111-
<div
112-
v-if="currentUser"
113-
class="text-body-2 text-medium-emphasis"
114-
>
115-
{{ userEmail }}
116-
</div>
117-
</div>
118-
119-
<v-divider />
120-
121-
<!-- Menu Items -->
122-
<div>
123-
<v-list-item
124-
v-for="item in menu"
125-
:key="item.title"
126-
:value="item"
127-
:data-test="item.title"
128-
:prepend-icon="item.icon"
129-
@click="triggerClick(item)"
130-
>
131-
<v-list-item-title class="font-weight-medium">
132-
{{ item.title }}
133-
</v-list-item-title>
134-
</v-list-item>
135-
</div>
136-
137-
<v-divider />
138-
139-
<!-- Dark Mode Toggle -->
140-
<v-list-item
141-
@click="toggleDarkMode"
142-
>
143-
<template #prepend>
144-
<v-icon
145-
:icon="isDarkMode ? 'mdi-brightness-6' : 'mdi-brightness-6'"
146-
size="small"
147-
/>
148-
</template>
149-
<v-list-item-title class="font-weight-medium">
150-
{{ isDarkMode ? 'Dark Mode' : 'Light Mode' }}
151-
</v-list-item-title>
152-
<template #append>
153-
<v-switch
154-
:model-value="isDarkMode"
155-
data-test="dark-mode-switch"
156-
color="primary"
157-
density="comfortable"
158-
false-icon="mdi-weather-sunny"
159-
true-icon="mdi-weather-night"
160-
hide-details
161-
readonly
162-
/>
163-
</template>
164-
</v-list-item>
165-
</v-list>
166-
</v-card>
167-
</v-menu>
168-
</div>
169-
</v-app-bar>
47+
<UserMenu
48+
:user-email="userEmail"
49+
:display-name="currentUser"
50+
:menu-items="menu"
51+
:is-dark-mode="isDarkMode"
52+
@select="handleUserMenuSelect"
53+
@toggle-dark-mode="toggleDarkMode"
54+
/>
55+
</template>
56+
</AppBarContent>
17057
</template>
17158

17259
<script setup lang="ts">
@@ -177,7 +64,8 @@ import {
17764
import { useRouter, useRoute, RouteLocationRaw, RouteLocation } from "vue-router";
17865
import { useChatWoot } from "@productdevbook/chatwoot/vue";
17966
import handleError from "@/utils/handleError";
180-
import UserIcon from "../User/UserIcon.vue";
67+
import AppBarContent from "@/components/AppBar/AppBarContent.vue";
68+
import UserMenu from "@/components/AppBar/UserMenu.vue";
18169
import DevicesDropdown from "./DevicesDropdown.vue";
18270
import InvitationsMenu from "@/components/Invitations/InvitationsMenu.vue";
18371
import PaywallChat from "../User/PaywallChat.vue";
@@ -249,6 +137,10 @@ const triggerClick = async (item: MenuItem) => {
249137
}
250138
};
251139
140+
const handleUserMenuSelect = (item: unknown) => {
141+
void triggerClick(item as MenuItem);
142+
};
143+
252144
const logout = async () => {
253145
try {
254146
await router.push({ name: "Login" });

0 commit comments

Comments
 (0)