feat(lite): settings page (#6418)
This commit is contained in:
parent
b2cebbfaf4
commit
c7227d2f50
@ -3,6 +3,7 @@
|
||||
## **0.2.0**
|
||||
|
||||
- Invalidate sessionId token after logout (PR [#6480](https://github.com/vatesfr/xen-orchestra/pull/6480))
|
||||
- Settings page (PR [#6418](https://github.com/vatesfr/xen-orchestra/pull/6418))
|
||||
|
||||
## **0.1.0**
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "@xen-orchestra/lite",
|
||||
"version": "0.0.0",
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev": "GIT_HEAD=$(git rev-parse HEAD) vite",
|
||||
"build": "run-p type-check build-only",
|
||||
"preview": "vite preview --port 4173",
|
||||
"build-only": "vite build",
|
||||
"build-only": "GIT_HEAD=$(git rev-parse HEAD) vite build",
|
||||
"deploy": "./scripts/deploy.sh",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
|
||||
|
@ -11,3 +11,17 @@ body {
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
color: var(--color-blue-scale-100);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-extra-blue-base);
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.card-view {
|
||||
padding: 1.2rem;
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
@ -6,7 +6,9 @@
|
||||
<UiIcon :icon="faAngleDown" class="dropdown-icon" />
|
||||
</button>
|
||||
</template>
|
||||
<MenuItem :icon="faGear">{{ $t("settings") }}</MenuItem>
|
||||
<MenuItem :icon="faGear" @click="openSettings">{{
|
||||
$t("settings")
|
||||
}}</MenuItem>
|
||||
<MenuItem :icon="faMessage" @click="openFeedbackUrl">
|
||||
{{ $t("send-us-feedback") }}
|
||||
</MenuItem>
|
||||
@ -50,6 +52,8 @@ const openFeedbackUrl = () => {
|
||||
"noopener"
|
||||
);
|
||||
};
|
||||
|
||||
const openSettings = () => router.push({ name: "settings" });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -5,50 +5,13 @@
|
||||
</RouterLink>
|
||||
<slot />
|
||||
<div class="right">
|
||||
<FontAwesomeIcon
|
||||
:icon="colorModeIcon"
|
||||
style="font-size: 1.5em; cursor: pointer"
|
||||
@click="toggleTheme"
|
||||
/>
|
||||
<FormWidget :before="faEarthAmericas">
|
||||
<select v-model="$i18n.locale">
|
||||
<option v-for="locale in $i18n.availableLocales" :key="locale">
|
||||
{{ locale }}
|
||||
</option>
|
||||
</select>
|
||||
</FormWidget>
|
||||
<AccountButton />
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRouter } from "vue-router";
|
||||
import {
|
||||
faEarthAmericas,
|
||||
faMoon,
|
||||
faSun,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
import AccountButton from "@/components/AccountButton.vue";
|
||||
import FormWidget from "@/components/FormWidget.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const { locale } = useI18n();
|
||||
|
||||
watch(locale, (newLocale) => localStorage.setItem("lang", newLocale));
|
||||
|
||||
const colorMode = useLocalStorage<string>("colorMode", "dark");
|
||||
const toggleTheme = () => {
|
||||
colorMode.value = document.documentElement.classList.toggle("dark")
|
||||
? "dark"
|
||||
: "light";
|
||||
};
|
||||
const colorModeIcon = computed(() =>
|
||||
colorMode.value === "light" ? faMoon : faSun
|
||||
);
|
||||
import AccountButton from '@/components/AccountButton.vue'
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
|
14
@xen-orchestra/lite/src/components/ui/UiKeyValueList.vue
Normal file
14
@xen-orchestra/lite/src/components/ui/UiKeyValueList.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<div class="ui-key-value-list"><slot /></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.ui-key-value-list {
|
||||
margin-top: 2rem;
|
||||
font-size: 1.4rem;
|
||||
/* UiKeyValueRow: 15em (key) + 15em (value) + 1rem (gap) */
|
||||
min-width: calc(30em + 1rem);
|
||||
}
|
||||
</style>
|
37
@xen-orchestra/lite/src/components/ui/UiKeyValueRow.vue
Normal file
37
@xen-orchestra/lite/src/components/ui/UiKeyValueRow.vue
Normal file
@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div class="ui-key-value-row">
|
||||
<span class="key" v-if="$slots.key">
|
||||
<slot name="key" />
|
||||
</span>
|
||||
<span class="value">
|
||||
<slot name="value" />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.ui-key-value-row {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: baseline;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
.key {
|
||||
color: var(--color-blue-scale-300);
|
||||
width: 15%;
|
||||
max-width: 15em;
|
||||
max-width: 30em;
|
||||
}
|
||||
.value {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.key,
|
||||
.value {
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
min-width: 15em;
|
||||
max-width: 30em;
|
||||
}
|
||||
</style>
|
@ -2,6 +2,24 @@ import { createI18n } from "vue-i18n";
|
||||
import en from "@/locales/en.json";
|
||||
import fr from "@/locales/fr.json";
|
||||
|
||||
interface Locales {
|
||||
[key: string]: {
|
||||
code: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const locales: Locales = {
|
||||
en: {
|
||||
code: "en",
|
||||
name: "English",
|
||||
},
|
||||
fr: {
|
||||
code: "fr",
|
||||
name: "Français",
|
||||
},
|
||||
};
|
||||
|
||||
export default createI18n<[typeof en], "en" | "fr">({
|
||||
locale: localStorage.getItem("lang") ?? "en",
|
||||
fallbackLocale: "en",
|
||||
|
@ -4,26 +4,34 @@
|
||||
"add-or": "+OR",
|
||||
"add-sort": "Add sort",
|
||||
"alarms": "Alarms",
|
||||
"appearance": "Appearance",
|
||||
"ascending": "ascending",
|
||||
"available-properties-for-advanced-filter": "Available properties for advanced filter:",
|
||||
"backup": "Backup",
|
||||
"cancel": "Cancel",
|
||||
"change-power-state": "Change power state",
|
||||
"community": "Community",
|
||||
"community-name": "{name} community",
|
||||
"copy": "Copy",
|
||||
"cpu-usage":"CPU usage",
|
||||
"dark-mode": "Dark mode",
|
||||
"dashboard": "Dashboard",
|
||||
"delete": "Delete",
|
||||
"descending": "descending",
|
||||
"display": "Display",
|
||||
"edit-config": "Edit config",
|
||||
"export": "Export",
|
||||
"export-table-to": "Export table to {type}",
|
||||
"export-vms": "Export VMs",
|
||||
"hosts": "Hosts",
|
||||
"language": "Language",
|
||||
"loading-hosts": "Loading hosts…",
|
||||
"log-out": "Log out",
|
||||
"login": "Login",
|
||||
"migrate": "Migrate",
|
||||
"network": "Network",
|
||||
"news": "News",
|
||||
"news-name": "{name} news",
|
||||
"or": "Or",
|
||||
"password": "Password",
|
||||
"property": "Property",
|
||||
@ -41,5 +49,6 @@
|
||||
"top-#": "Top {n}",
|
||||
"total-free": "Total free",
|
||||
"total-used": "Total used",
|
||||
"version": "Version",
|
||||
"vms": "VMs"
|
||||
}
|
||||
|
@ -4,26 +4,34 @@
|
||||
"add-or": "+OU",
|
||||
"add-sort": "Ajouter un tri",
|
||||
"alarms": "Alarmes",
|
||||
"appearance": "Apparence",
|
||||
"ascending": "ascendant",
|
||||
"available-properties-for-advanced-filter": "Propriétés disponibles pour le filtrage avancé :",
|
||||
"backup": "Sauvegarde",
|
||||
"cancel": "Annuler",
|
||||
"change-power-state": "Changer l'état d'alimentation",
|
||||
"community": "Communauté",
|
||||
"community-name": "Communauté {name}",
|
||||
"copy": "Copier",
|
||||
"cpu-usage":"Utilisation CPU",
|
||||
"dark-mode": "Mode sombre",
|
||||
"dashboard": "Tableau de bord",
|
||||
"delete": "Supprimer",
|
||||
"descending": "descendant",
|
||||
"display": "Affichage",
|
||||
"edit-config": "Modifier config",
|
||||
"export": "Exporter",
|
||||
"export-table-to": "Exporter le tableau en {type}",
|
||||
"export-vms": "Exporter les VMs",
|
||||
"hosts": "Hôtes",
|
||||
"language": "Langue",
|
||||
"loading-hosts": "Chargement des hôtes…",
|
||||
"log-out": "Se déconnecter",
|
||||
"login": "Connexion",
|
||||
"migrate": "Migrer",
|
||||
"network": "Réseau",
|
||||
"news": "Actualités",
|
||||
"news-name": "Actualités {name}",
|
||||
"or": "Ou",
|
||||
"password": "Mot de passe",
|
||||
"property": "Propriété",
|
||||
@ -41,5 +49,6 @@
|
||||
"top-#": "Top {n}",
|
||||
"total-free": "Total libre",
|
||||
"total-used": "Total utilisé",
|
||||
"version": "Version",
|
||||
"vms": "VMs"
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import pool from "@/router/pool";
|
||||
import HomeView from "@/views/HomeView.vue";
|
||||
import HostDashboardView from "@/views/host/HostDashboardView.vue";
|
||||
import HostRootView from "@/views/host/HostRootView.vue";
|
||||
import SettingsView from "@/views/settings/SettingsView.vue";
|
||||
import VmConsoleView from "@/views/vm/VmConsoleView.vue";
|
||||
import VmRootView from "@/views/vm/VmRootView.vue";
|
||||
|
||||
@ -14,6 +15,11 @@ const router = createRouter({
|
||||
name: "home",
|
||||
component: HomeView,
|
||||
},
|
||||
{
|
||||
path: "/settings",
|
||||
name: "settings",
|
||||
component: SettingsView,
|
||||
},
|
||||
pool,
|
||||
{
|
||||
path: "/host/:uuid",
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="pool-dashboard-view">
|
||||
<div class="pool-dashboard-view card-view">
|
||||
<PoolDashboardStatus class="item" />
|
||||
<PoolDashboardStorageUsage class="item" />
|
||||
<PoolDashboardCpuUsage class="item" />
|
||||
@ -64,11 +64,6 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.pool-dashboard-view {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.item {
|
||||
min-width: 37rem;
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="pool-root-view">
|
||||
<PoolHeader />
|
||||
<PoolTabBar />
|
||||
<div class="view">
|
||||
<div class="card-view">
|
||||
<RouterView />
|
||||
</div>
|
||||
</div>
|
||||
@ -13,8 +13,4 @@ import PoolHeader from "@/components/pool/PoolHeader.vue";
|
||||
import PoolTabBar from "@/components/pool/PoolTabBar.vue";
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.view {
|
||||
padding: 2rem;
|
||||
}
|
||||
</style>
|
||||
<style lang="postcss" scoped></style>
|
||||
|
124
@xen-orchestra/lite/src/views/settings/SettingsView.vue
Normal file
124
@xen-orchestra/lite/src/views/settings/SettingsView.vue
Normal file
@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<TitleBar :icon="faGear">{{ $t("settings") }}</TitleBar>
|
||||
<div class="card-view">
|
||||
<UiCard class="group">
|
||||
<UiTitle type="h4">Xen Orchestra Lite</UiTitle>
|
||||
<UiKeyValueList>
|
||||
<UiKeyValueRow>
|
||||
<template #key>{{ $t("version") }}</template>
|
||||
<template #value
|
||||
>v{{ version
|
||||
}}<code v-if="gitHead"> ({{ gitHead.slice(0, 5) }})</code></template
|
||||
>
|
||||
</UiKeyValueRow>
|
||||
<UiKeyValueRow>
|
||||
<template #key>{{ $t("news") }}</template>
|
||||
<template #value
|
||||
><a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://xcp-ng.org/blog/"
|
||||
>{{ $t("news-name", { name: "XCP-ng" }) }}</a
|
||||
> - <a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://xen-orchestra.com/blog/"
|
||||
>{{ $t("news-name", { name: "Xen Orchestra" }) }}</a
|
||||
></template
|
||||
>
|
||||
</UiKeyValueRow>
|
||||
<UiKeyValueRow>
|
||||
<template #key>{{ $t("community") }}</template>
|
||||
<template #value
|
||||
><a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://xcp-ng.org/forum"
|
||||
>{{ $t("community-name", { name: "XCP-ng" }) }}</a
|
||||
> - <a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://xcp-ng.org/forum/category/12/xen-orchestra"
|
||||
>{{ $t("community-name", { name: "Xen Orchestra" }) }}</a
|
||||
></template
|
||||
>
|
||||
</UiKeyValueRow>
|
||||
</UiKeyValueList>
|
||||
</UiCard>
|
||||
<UiCard class="group">
|
||||
<UiTitle type="h4">{{ $t("display") }}</UiTitle>
|
||||
<UiKeyValueList>
|
||||
<UiKeyValueRow>
|
||||
<template #key>{{ $t("appearance") }}</template>
|
||||
<template #value
|
||||
><FormLabel>
|
||||
<FormToggle
|
||||
:modelValue="darkMode"
|
||||
@update:modelValue="setDarkMode"
|
||||
/>{{ $t("dark-mode") }}</FormLabel
|
||||
></template
|
||||
>
|
||||
</UiKeyValueRow>
|
||||
</UiKeyValueList>
|
||||
</UiCard>
|
||||
<UiCard class="group">
|
||||
<UiTitle type="h4">{{ $t("language") }}</UiTitle>
|
||||
<UiKeyValueList>
|
||||
<UiKeyValueRow>
|
||||
<template #value>
|
||||
<FormWidget class="full-length" :before="faEarthAmericas">
|
||||
<select v-model="$i18n.locale">
|
||||
<option
|
||||
:value="locale"
|
||||
v-for="locale in $i18n.availableLocales"
|
||||
:key="locale"
|
||||
>
|
||||
{{ locales[locale].name ?? locale }}
|
||||
</option>
|
||||
</select>
|
||||
</FormWidget></template
|
||||
>
|
||||
</UiKeyValueRow>
|
||||
</UiKeyValueList>
|
||||
</UiCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { locales } from "@/i18n";
|
||||
import { faEarthAmericas, faGear } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
import FormWidget from "@/components/FormWidget.vue";
|
||||
import TitleBar from "@/components/TitleBar.vue";
|
||||
import FormLabel from "@/components/form/FormLabel.vue";
|
||||
import FormToggle from "@/components/form/FormToggle.vue";
|
||||
import UiCard from "@/components/ui/UiCard.vue";
|
||||
import UiKeyValueList from "@/components/ui/UiKeyValueList.vue";
|
||||
import UiKeyValueRow from "@/components/ui/UiKeyValueRow.vue";
|
||||
import UiTitle from "@/components/ui/UiTitle.vue";
|
||||
|
||||
const version = XO_LITE_VERSION;
|
||||
const gitHead = XO_LITE_GIT_HEAD;
|
||||
const { locale } = useI18n();
|
||||
|
||||
watch(locale, (newLocale) => localStorage.setItem("lang", newLocale));
|
||||
|
||||
const colorMode = useLocalStorage<string>("colorMode", "dark");
|
||||
const darkMode = computed(() => colorMode.value !== "light");
|
||||
const setDarkMode = (enabled: boolean) => {
|
||||
colorMode.value = enabled ? "dark" : "light";
|
||||
document.documentElement.classList[enabled ? "add" : "remove"]("dark");
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.card-view {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.full-length {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
@ -6,6 +6,10 @@ import vue from "@vitejs/plugin-vue";
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue(), vueI18n()],
|
||||
define: {
|
||||
XO_LITE_VERSION: JSON.stringify(process.env.npm_package_version),
|
||||
XO_LITE_GIT_HEAD: JSON.stringify(process.env.GIT_HEAD),
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
|
Loading…
Reference in New Issue
Block a user