Skip to content

Commit 3f4b67b

Browse files
committed
add tabs component
1 parent 5ff1889 commit 3f4b67b

File tree

1 file changed

+150
-0
lines changed

1 file changed

+150
-0
lines changed
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
<script setup lang="ts">
2+
import { getRouteName, useDashboard } from '@/composables/dashboard';
3+
import { useMetrics } from '@/composables/metrics';
4+
import { useRoute } from 'vue-router';
5+
import type { AppInfo } from '@/types/dashboard';
6+
import { computed, onBeforeUnmount, onMounted, nextTick, useTemplateRef, type PropType } from 'vue';
7+
import { useRouter } from '@/router';
8+
9+
const props = defineProps({
10+
appInfo: { type: Object as PropType<AppInfo>, required: true },
11+
});
12+
13+
const { dashboard, getMode } = useDashboard();
14+
const { sum } = useMetrics();
15+
const tabsContainer = useTemplateRef<HTMLElement>('tabsContainer');
16+
const dropdownMenu = useTemplateRef<HTMLElement>('dropdownMenu');
17+
const moreTabs = useTemplateRef<HTMLElement>('moreTabs');
18+
const tabItems = computed(() => [
19+
{ text: 'Overview', isVisible: true, to: { name: getRouteName('dashboard').value }, cssClass: 'overview' },
20+
{ text: 'HTTP', isVisible: isServiceAvailable('http'), to: { name: getRouteName('http').value } },
21+
{ text: 'Kafka', isVisible: isServiceAvailable('kafka'), to: { name: getRouteName('kafka').value } },
22+
{ text: 'Mail', isVisible: isServiceAvailable('mail'), to: { name: getRouteName('mail').value } },
23+
{ text: 'LDAP', isVisible: isServiceAvailable('ldap'), to: { name: getRouteName('ldap').value } },
24+
{ text: 'Jobs', isVisible: hasJobs.value, to: { name: getRouteName('jobs').value } },
25+
{ text: 'Configs', isVisible: true, to: { name: getRouteName('configs').value } },
26+
{ text: 'Faker', isVisible: getMode() === 'live', to: { name: getRouteName('tree').value } },
27+
{ text: 'Search', isVisible: props.appInfo.search.enabled, to: { name: getRouteName('search').value } },
28+
]);
29+
const route = useRoute();
30+
const router = useRouter();
31+
const tabActive = computed(() => tabItems.value.find(x => x.text !== 'Overview' && route.matched.some(r => r.name === x.to.name)) ?? tabItems.value[0])
32+
33+
const response = dashboard.value.getMetrics('app')
34+
const hasJobs = computed(() => {
35+
return sum(response.data, 'app_job_run_total') > 0
36+
})
37+
38+
const handleResize = () => updateTabsResponsiveness();
39+
40+
onMounted(async () => {
41+
// Wait for Vue to finish rendering the DOM
42+
await nextTick();
43+
window.addEventListener("resize", handleResize);
44+
updateTabsResponsiveness();
45+
});
46+
47+
onBeforeUnmount(() => {
48+
window.removeEventListener("resize", handleResize);
49+
});
50+
51+
function isServiceAvailable(service: string): boolean{
52+
if (!props.appInfo.activeServices){
53+
return false
54+
}
55+
return props.appInfo.activeServices.includes(service)
56+
}
57+
58+
function updateTabsResponsiveness() {
59+
const container = tabsContainer.value;
60+
const dropdown = dropdownMenu.value;
61+
const more = moreTabs.value
62+
63+
if (!container || !dropdown || !more) {
64+
return
65+
}
66+
67+
// Reset everything
68+
const allTabs = [...container.querySelectorAll("li.nav-item:not(#moreTabs)")];
69+
for (const tab of allTabs) {
70+
tab.classList.remove('d-none');
71+
}
72+
for (const tab of [...dropdown.querySelectorAll("li")]) {
73+
tab.classList.add('d-none');
74+
}
75+
// Make sure it's measurable
76+
more.classList.remove("d-none");
77+
const moreWidth = more.offsetWidth;
78+
more.classList.add("d-none");
79+
80+
const documentWidth = document.body.clientWidth * 0.9;
81+
let usedWidth = moreWidth;
82+
let hidden = 0;
83+
84+
for (const [index, item] of allTabs.entries()) {
85+
const tab = item as HTMLElement
86+
87+
// do not show the more dropdown only for one item
88+
if (index === (allTabs.length-1) && hidden === 0) {
89+
break;
90+
}
91+
92+
if (usedWidth + tab.offsetWidth > documentWidth) {
93+
tab.classList.add("d-none");
94+
const dropItem = Array.from(dropdown.querySelectorAll("li"))
95+
.find(el => el.textContent.trim() === tab.innerText);
96+
if (dropItem) {
97+
dropItem.classList.remove("d-none");
98+
}
99+
more.classList.remove("d-none");
100+
hidden++;
101+
} else {
102+
usedWidth += tab.offsetWidth;
103+
}
104+
}
105+
106+
const activeTab = allTabs.find(tab =>
107+
tab.querySelector('.nav-link.router-link-exact-active')
108+
);
109+
const moreLink = more.querySelector('.nav-link')!;
110+
// If the active tab is hidden => it's in the dropdown
111+
if (moreLink && activeTab && activeTab.classList.contains('d-none')) {
112+
// Get the label text of the active tab
113+
const activeLabel = activeTab.querySelector('.nav-link')!.textContent.trim();
114+
// Replace "More" text with active tab label
115+
moreLink.textContent = activeLabel;
116+
moreLink.classList.add('router-link-exact-active')
117+
118+
} else {
119+
// Restore default
120+
moreLink.textContent = 'More';
121+
moreLink.classList.remove('router-link-exact-active')
122+
}
123+
};
124+
</script>
125+
126+
<template>
127+
<nav class="navbar navbar-expand pb-1" aria-label="Services">
128+
<div>
129+
<ul class="navbar-nav me-auto mb-0" ref="tabsContainer">
130+
<template v-for="tabItem in tabItems" :key="tabItem.text">
131+
<li class="nav-item" :class="tabItem.cssClass ? tabItem.cssClass : ''" v-if="tabItem.isVisible">
132+
<router-link class="nav-link" :to="tabItem.to">{{ tabItem.text }}</router-link>
133+
</li>
134+
</template>
135+
<li id="moreTabs" class="nav-item dropdown d-none" ref="moreTabs">
136+
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false">{{ tabActive?.text }}</a>
137+
<ul class="dropdown-menu" ref="dropdownMenu">
138+
<template v-for="tabItem in tabItems" :key="tabItem.text">
139+
<li>
140+
<a :href="router.resolve(tabItem.to).href" class="dropdown-item">
141+
{{ tabItem.text }}
142+
</a>
143+
</li>
144+
</template>
145+
</ul>
146+
</li>
147+
</ul>
148+
</div>
149+
</nav>
150+
</template>

0 commit comments

Comments
 (0)