feat(lite): dynamic page title (#6853)

See #6793

ℹ️ This PR adds a `pageTitleStore` which allows defining the current page title
according to 3 parts: an object, a string, and a count. Each part is optional.

 The page title is **reactive** when function argument is a `Ref`, a `Computed`
or a getter. For example, when updating a VM name, the page title will be
updated in every tabs.

🪄 Each title part is automatically unset when the component that set it is
unmounted.
This commit is contained in:
Thierry Goettelmann
2023-07-27 11:41:33 +02:00
committed by GitHub
parent 3b1bcc67ae
commit 20d04ba956
32 changed files with 225 additions and 38 deletions

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
<title>XO Lite</title>
</head>
<body>
<div id="root"></div>

View File

@@ -41,8 +41,6 @@ if (link == null) {
}
link.href = favicon;
document.title = "XO Lite";
const xenApiStore = useXenApiStore();
const { pool } = usePoolStore().subscribe();
useChartTheme();

View File

@@ -24,6 +24,7 @@
</template>
<script lang="ts" setup>
import { usePageTitleStore } from "@/stores/page-title.store";
import { storeToRefs } from "pinia";
import { onMounted, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
@@ -33,6 +34,7 @@ import UiButton from "@/components/ui/UiButton.vue";
import { useXenApiStore } from "@/stores/xen-api.store";
const { t } = useI18n();
usePageTitleStore().setTitle(t("login"));
const xenApiStore = useXenApiStore();
const { isConnecting } = storeToRefs(xenApiStore);
const login = ref("root");

View File

@@ -77,6 +77,7 @@
"news": "News",
"news-name": "{name} news",
"new-features-are-coming": "New features are coming soon!",
"not-found": "Not found",
"object": "Object",
"object-not-found": "Object {id} can't be found…",
"or": "Or",
@@ -127,7 +128,6 @@
"system": "System",
"task": {
"estimated-end": "Estimated end",
"page-title": "Tasks | (1) Tasks | ({n}) Tasks",
"progress": "Progress",
"started": "Started"
},

View File

@@ -77,6 +77,7 @@
"news": "Actualités",
"news-name": "Actualités {name}",
"new-features-are-coming": "De nouvelles fonctionnalités arrivent bientôt !",
"not-found": "Non trouvé",
"object": "Objet",
"object-not-found": "L'objet {id} est introuvable…",
"or": "Ou",
@@ -127,7 +128,6 @@
"system": "Système",
"task": {
"estimated-end": "Fin estimée",
"page-title": "Tâches | (1) Tâches | ({n}) Tâches",
"progress": "Progression",
"started": "Démarré"
},

View File

@@ -33,7 +33,7 @@ const router = createRouter({
},
{
path: "/:pathMatch(.*)*",
name: "notFound",
name: "not-found",
component: () => import("@/views/PageNotFoundView.vue"),
},
],

View File

@@ -0,0 +1,92 @@
import { useTitle } from "@vueuse/core";
import { defineStore } from "pinia";
import {
computed,
type MaybeRefOrGetter,
onBeforeUnmount,
reactive,
toRef,
watch,
} from "vue";
const PAGE_TITLE_SUFFIX = "XO Lite";
interface PageTitleConfig {
object: { name_label: string } | undefined;
title: string | undefined;
count: number | undefined;
}
export const usePageTitleStore = defineStore("page-title", () => {
const pageTitleConfig = reactive<PageTitleConfig>({
count: undefined,
title: undefined,
object: undefined,
});
const generatedPageTitle = computed(() => {
const { object, title, count } = pageTitleConfig;
const parts = [];
if (count !== undefined && count > 0) {
parts.push(`(${count})`);
}
if (title !== undefined && object !== undefined) {
parts.push(`${title} - ${object.name_label}`);
} else if (title !== undefined) {
parts.push(title);
} else if (object !== undefined) {
parts.push(object.name_label);
}
if (parts.length === 0) {
return undefined;
}
return parts.join(" ");
});
useTitle(generatedPageTitle, {
titleTemplate: computed(() =>
generatedPageTitle.value === undefined
? PAGE_TITLE_SUFFIX
: `%s - ${PAGE_TITLE_SUFFIX}`
),
});
const setPageTitleConfig = <T extends keyof PageTitleConfig>(
configKey: T,
value: MaybeRefOrGetter<PageTitleConfig[T]>
) => {
const stop = watch(
toRef(value),
(newValue) =>
(pageTitleConfig[configKey] = newValue as PageTitleConfig[T]),
{
immediate: true,
}
);
onBeforeUnmount(() => {
stop();
pageTitleConfig[configKey] = undefined;
});
};
const setObject = (
object: MaybeRefOrGetter<{ name_label: string } | undefined>
) => setPageTitleConfig("object", object);
const setTitle = (title: MaybeRefOrGetter<string | undefined>) =>
setPageTitleConfig("title", title);
const setCount = (count: MaybeRefOrGetter<number | undefined>) =>
setPageTitleConfig("count", count);
return {
setObject,
setTitle,
setCount,
};
});

View File

@@ -9,6 +9,8 @@
</template>
<script setup lang="ts">
import { usePageTitleStore } from "@/stores/page-title.store";
import { useI18n } from "vue-i18n";
import { useRouter } from "vue-router";
import UiButton from "@/components/ui/UiButton.vue";
@@ -16,6 +18,8 @@ defineProps<{
id: string;
}>();
usePageTitleStore().setTitle(useI18n().t("not-found"));
const router = useRouter();
</script>

View File

@@ -10,10 +10,13 @@
</template>
<script setup lang="ts">
import { useRouter } from "vue-router";
import UiButton from "@/components/ui/UiButton.vue";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useI18n } from "vue-i18n";
import { useRouter } from "vue-router";
const router = useRouter();
usePageTitleStore().setTitle(useI18n().t("not-found"));
</script>
<style lang="postcss" scoped>

View File

@@ -6,6 +6,7 @@
</template>
<script lang="ts" setup>
import { usePageTitleStore } from "@/stores/page-title.store";
import { computed } from "vue";
import { useRouter } from "vue-router";
import { faBook } from "@fortawesome/free-solid-svg-icons";
@@ -20,6 +21,8 @@ const title = computed(() => {
return `${currentRoute.value.meta.storyTitle} Story`;
});
usePageTitleStore().setTitle(title);
</script>
<style lang="postcss" scoped></style>

View File

@@ -4,4 +4,8 @@
<script lang="ts" setup>
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useI18n } from "vue-i18n";
usePageTitleStore().setTitle(useI18n().t("dashboard"));
</script>

View File

@@ -8,17 +8,22 @@
import ObjectNotFoundWrapper from "@/components/ObjectNotFoundWrapper.vue";
import type { XenApiHost } from "@/libs/xen-api";
import { useHostStore } from "@/stores/host.store";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useUiStore } from "@/stores/ui.store";
import { watchEffect } from "vue";
import { computed, watchEffect } from "vue";
import { useRoute } from "vue-router";
const { hasUuid, isReady, getByUuid } = useHostStore().subscribe();
const route = useRoute();
const uiStore = useUiStore();
const currentHost = computed(() =>
getByUuid(route.params.uuid as XenApiHost["uuid"])
);
watchEffect(() => {
uiStore.currentHostOpaqueRef = getByUuid(
route.params.uuid as XenApiHost["uuid"]
)?.$ref;
uiStore.currentHostOpaqueRef = currentHost.value?.$ref;
});
usePageTitleStore().setObject(currentHost);
</script>

View File

@@ -4,4 +4,8 @@
<script lang="ts" setup>
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useI18n } from "vue-i18n";
usePageTitleStore().setTitle(useI18n().t("alarms"));
</script>

View File

@@ -31,8 +31,23 @@ export const N_ITEMS = 5;
</script>
<script lang="ts" setup>
import PoolCpuUsageChart from "@/components/pool/dashboard/cpuUsage/PoolCpuUsageChart.vue";
import PoolDashboardCpuProvisioning from "@/components/pool/dashboard/PoolDashboardCpuProvisioning.vue";
import PoolDashboardCpuUsage from "@/components/pool/dashboard/PoolDashboardCpuUsage.vue";
import PoolDashboardNetworkChart from "@/components/pool/dashboard/PoolDashboardNetworkChart.vue";
import PoolDashboardRamUsage from "@/components/pool/dashboard/PoolDashboardRamUsage.vue";
import PoolDashboardStatus from "@/components/pool/dashboard/PoolDashboardStatus.vue";
import PoolDashboardStorageUsage from "@/components/pool/dashboard/PoolDashboardStorageUsage.vue";
import PoolDashboardRamUsageChart from "@/components/pool/dashboard/ramUsage/PoolRamUsage.vue";
import UiCardComingSoon from "@/components/ui/UiCardComingSoon.vue";
import UiCardGroup from "@/components/ui/UiCardGroup.vue";
import useFetchStats from "@/composables/fetch-stats.composable";
import { GRANULARITY, type HostStats, type VmStats } from "@/libs/xapi-stats";
import type { XenApiHost, XenApiVm } from "@/libs/xen-api";
import { useHostMetricsStore } from "@/stores/host-metrics.store";
import { useHostStore } from "@/stores/host.store";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useVmStore } from "@/stores/vm.store";
import {
IK_HOST_LAST_WEEK_STATS,
IK_HOST_STATS,
@@ -40,20 +55,9 @@ import {
} from "@/types/injection-keys";
import { differenceBy } from "lodash-es";
import { provide, watch } from "vue";
import UiCardComingSoon from "@/components/ui/UiCardComingSoon.vue";
import PoolCpuUsageChart from "@/components/pool/dashboard/cpuUsage/PoolCpuUsageChart.vue";
import PoolDashboardCpuUsage from "@/components/pool/dashboard/PoolDashboardCpuUsage.vue";
import PoolDashboardNetworkChart from "@/components/pool/dashboard/PoolDashboardNetworkChart.vue";
import PoolDashboardCpuProvisioning from "@/components/pool/dashboard/PoolDashboardCpuProvisioning.vue";
import PoolDashboardRamUsage from "@/components/pool/dashboard/PoolDashboardRamUsage.vue";
import PoolDashboardRamUsageChart from "@/components/pool/dashboard/ramUsage/PoolRamUsage.vue";
import PoolDashboardStatus from "@/components/pool/dashboard/PoolDashboardStatus.vue";
import PoolDashboardStorageUsage from "@/components/pool/dashboard/PoolDashboardStorageUsage.vue";
import useFetchStats from "@/composables/fetch-stats.composable";
import { GRANULARITY, type HostStats, type VmStats } from "@/libs/xapi-stats";
import type { XenApiHost, XenApiVm } from "@/libs/xen-api";
import { useHostStore } from "@/stores/host.store";
import { useVmStore } from "@/stores/vm.store";
import { useI18n } from "vue-i18n";
usePageTitleStore().setTitle(useI18n().t("dashboard"));
const hostMetricsSubscription = useHostMetricsStore().subscribe();

View File

@@ -4,4 +4,8 @@
<script lang="ts" setup>
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useI18n } from "vue-i18n";
usePageTitleStore().setTitle(useI18n().t("hosts"));
</script>

View File

@@ -4,4 +4,8 @@
<script lang="ts" setup>
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useI18n } from "vue-i18n";
usePageTitleStore().setTitle(useI18n().t("network"));
</script>

View File

@@ -10,6 +10,11 @@
<script lang="ts" setup>
import PoolHeader from "@/components/pool/PoolHeader.vue";
import PoolTabBar from "@/components/pool/PoolTabBar.vue";
import { usePoolStore } from "@/stores/pool.store";
import { usePageTitleStore } from "@/stores/page-title.store";
const { pool } = usePoolStore().subscribe();
usePageTitleStore().setObject(pool);
</script>
<style lang="postcss" scoped></style>

View File

@@ -4,4 +4,8 @@
<script lang="ts" setup>
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useI18n } from "vue-i18n";
usePageTitleStore().setTitle(useI18n().t("stats"));
</script>

View File

@@ -4,4 +4,8 @@
<script lang="ts" setup>
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useI18n } from "vue-i18n";
usePageTitleStore().setTitle(useI18n().t("storage"));
</script>

View File

@@ -4,4 +4,8 @@
<script lang="ts" setup>
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useI18n } from "vue-i18n";
usePageTitleStore().setTitle(useI18n().t("system"));
</script>

View File

@@ -21,8 +21,7 @@ import useFilteredCollection from "@/composables/filtered-collection.composable"
import useSortedCollection from "@/composables/sorted-collection.composable";
import type { XenApiTask } from "@/libs/xen-api";
import { useTaskStore } from "@/stores/task.store";
import { useTitle } from "@vueuse/core";
import { computed } from "vue";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useI18n } from "vue-i18n";
const { records, hasError } = useTaskStore().subscribe();
@@ -53,9 +52,9 @@ const finishedTasks = useArrayRemovedItemsHistory(
}
);
useTitle(
computed(() => t("task.page-title", { n: pendingTasks.value.length }))
);
const titleStore = usePageTitleStore();
titleStore.setTitle(t("tasks"));
titleStore.setCount(() => pendingTasks.value.length);
</script>
<style lang="postcss" scoped>

View File

@@ -38,6 +38,7 @@ import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import VmsActionsBar from "@/components/vm/VmsActionsBar.vue";
import { POWER_STATE } from "@/libs/xen-api";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useUiStore } from "@/stores/ui.store";
import { useVmStore } from "@/stores/vm.store";
import type { Filters } from "@/types/filter";
@@ -46,9 +47,13 @@ import { storeToRefs } from "pinia";
import { ref } from "vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const titleStore = usePageTitleStore();
titleStore.setTitle(t("vms"));
const { records: vms } = useVmStore().subscribe();
const { isMobile, isDesktop } = storeToRefs(useUiStore());
const { t } = useI18n();
const filters: Filters = {
name_label: { label: t("name"), type: "string" },
@@ -62,6 +67,8 @@ const filters: Filters = {
};
const selectedVmsRefs = ref([]);
titleStore.setCount(() => selectedVmsRefs.value.length);
</script>
<style lang="postcss" scoped>

View File

@@ -157,6 +157,7 @@
</template>
<script lang="ts" setup>
import { usePageTitleStore } from "@/stores/page-title.store";
import { computed } from "vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
@@ -181,7 +182,9 @@ import UiKeyValueRow from "@/components/ui/UiKeyValueRow.vue";
const xoLiteVersion = XO_LITE_VERSION;
const xoLiteGitHead = XO_LITE_GIT_HEAD;
const { locale } = useI18n();
const { t, locale } = useI18n();
usePageTitleStore().setTitle(() => t("settings"));
const { pool } = usePoolStore().subscribe();
const { getByOpaqueRef: getHost } = useHostStore().subscribe();

View File

@@ -4,4 +4,8 @@
<script lang="ts" setup>
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useI18n } from "vue-i18n";
usePageTitleStore().setTitle(useI18n().t("alarms"));
</script>

View File

@@ -3,19 +3,21 @@
<div v-else-if="!isVmRunning">Console is only available for running VMs.</div>
<RemoteConsole
v-else-if="vm && vmConsole"
:location="vmConsole.location"
:is-console-available="!isOperationsPending(vm, STOP_OPERATIONS)"
:location="vmConsole.location"
/>
</template>
<script lang="ts" setup>
import { POWER_STATE, VM_OPERATION, type XenApiVm } from "@/libs/xen-api";
import { computed } from "vue";
import { useRoute } from "vue-router";
import RemoteConsole from "@/components/RemoteConsole.vue";
import { useConsoleStore } from "@/stores/console.store";
import { useVmStore } from "@/stores/vm.store";
import { isOperationsPending } from "@/libs/utils";
import { POWER_STATE, VM_OPERATION, type XenApiVm } from "@/libs/xen-api";
import { useConsoleStore } from "@/stores/console.store";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useVmStore } from "@/stores/vm.store";
import { computed } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
const STOP_OPERATIONS = [
VM_OPERATION.SHUTDOWN,
@@ -27,6 +29,8 @@ const STOP_OPERATIONS = [
VM_OPERATION.SUSPEND,
];
usePageTitleStore().setTitle(useI18n().t("console"));
const route = useRoute();
const { isReady: isVmReady, getByUuid: getVmByUuid } = useVmStore().subscribe();

View File

@@ -4,4 +4,8 @@
<script lang="ts" setup>
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useI18n } from "vue-i18n";
usePageTitleStore().setTitle(useI18n().t("dashboard"));
</script>

View File

@@ -4,4 +4,8 @@
<script lang="ts" setup>
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useI18n } from "vue-i18n";
usePageTitleStore().setTitle(useI18n().t("network"));
</script>

View File

@@ -11,6 +11,7 @@ import ObjectNotFoundWrapper from "@/components/ObjectNotFoundWrapper.vue";
import VmHeader from "@/components/vm/VmHeader.vue";
import VmTabBar from "@/components/vm/VmTabBar.vue";
import type { XenApiVm } from "@/libs/xen-api";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useUiStore } from "@/stores/ui.store";
import { useVmStore } from "@/stores/vm.store";
import { whenever } from "@vueuse/core";
@@ -22,4 +23,5 @@ const { getByUuid, hasUuid, isReady } = useVmStore().subscribe();
const uiStore = useUiStore();
const vm = computed(() => getByUuid(route.params.uuid as XenApiVm["uuid"]));
whenever(vm, (vm) => (uiStore.currentHostOpaqueRef = vm.resident_on));
usePageTitleStore().setObject(vm);
</script>

View File

@@ -4,4 +4,8 @@
<script lang="ts" setup>
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useI18n } from "vue-i18n";
usePageTitleStore().setTitle(useI18n().t("stats"));
</script>

View File

@@ -4,4 +4,8 @@
<script lang="ts" setup>
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useI18n } from "vue-i18n";
usePageTitleStore().setTitle(useI18n().t("storage"));
</script>

View File

@@ -4,4 +4,8 @@
<script lang="ts" setup>
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useI18n } from "vue-i18n";
usePageTitleStore().setTitle(useI18n().t("system"));
</script>

View File

@@ -4,4 +4,8 @@
<script lang="ts" setup>
import PageUnderConstruction from "@/components/PageUnderConstruction.vue";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useI18n } from "vue-i18n";
usePageTitleStore().setTitle(useI18n().t("tasks"));
</script>