mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-08-04 21:15:22 +02:00
Merge branch 'mealie-next' into mealie-next
This commit is contained in:
commit
b13d66108d
101 changed files with 1312 additions and 1300 deletions
|
@ -1,137 +0,0 @@
|
|||
<template>
|
||||
<v-container fluid class="md-container">
|
||||
<BannerExperimental></BannerExperimental>
|
||||
<BaseCardSectionTitle title="Site Analytics">
|
||||
Your instance of Mealie can send anonymous usage statistics to the Mealie project team. This is done to help us
|
||||
gauge the usage of mealie, provide public statistics and to help us improve the user experience.
|
||||
|
||||
<p class="pt-4 pb-0 mb-0">
|
||||
Your installation creates a UUID that is used to identify your installation,
|
||||
<strong> this is randomly generated using the UUID4 implementation in python</strong>. This UUID is stored on
|
||||
our analytics server and used to ensure your data is only counted once.
|
||||
</p>
|
||||
</BaseCardSectionTitle>
|
||||
<section>
|
||||
<BaseCardSectionTitle class="pb-0" :icon="$globals.icons.chart" title="Analytics Settings">
|
||||
When you opt into analytics your install will register itself with the Analytics API to count the installation
|
||||
and register your generated anonymous installation ID
|
||||
</BaseCardSectionTitle>
|
||||
<v-card-text>
|
||||
<v-switch v-model="state.analyticsEnabled" label="Collect Anonymous Analytics" />
|
||||
</v-card-text>
|
||||
</section>
|
||||
<section class="my-8">
|
||||
<BaseCardSectionTitle class="pb-0" :icon="$globals.icons.chart" title="Analytics Data">
|
||||
This is a list of all the data that is sent to the Mealie project team.
|
||||
</BaseCardSectionTitle>
|
||||
<v-card class="ma-2">
|
||||
<template v-for="(value, idx) in data">
|
||||
<v-list-item :key="`item-${idx}`">
|
||||
<v-list-item-title class="py-2">
|
||||
<div>{{ value.text }}</div>
|
||||
<v-list-item-subtitle class="text-end"> {{ getValue(value.valueKey) }} </v-list-item-subtitle>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-divider :key="`divider-${idx}`" class="mx-2"></v-divider>
|
||||
</template>
|
||||
</v-card>
|
||||
</section>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, useAsync } from "@nuxtjs/composition-api";
|
||||
import { useAdminApi } from "~/composables/api";
|
||||
import { useAsyncKey } from "~/composables/use-utils";
|
||||
import { MealieAnalytics } from "~/lib/api/types/analytics";
|
||||
|
||||
type DisplayData = {
|
||||
text: string;
|
||||
valueKey: keyof MealieAnalytics;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
layout: "admin",
|
||||
setup() {
|
||||
const adminApi = useAdminApi();
|
||||
|
||||
const state = reactive({
|
||||
analyticsEnabled: false,
|
||||
});
|
||||
|
||||
const analyticsData = useAsync(async () => {
|
||||
const { data } = await adminApi.analytics.getAnalytics();
|
||||
return data;
|
||||
}, useAsyncKey());
|
||||
|
||||
function getValue(key: keyof MealieAnalytics) {
|
||||
if (!analyticsData.value) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return analyticsData.value[key];
|
||||
}
|
||||
|
||||
const data: DisplayData[] = [
|
||||
{
|
||||
text: "Installation Id",
|
||||
valueKey: "installationId",
|
||||
},
|
||||
{
|
||||
text: "Version",
|
||||
valueKey: "version",
|
||||
},
|
||||
{
|
||||
text: "Database",
|
||||
valueKey: "databaseType",
|
||||
},
|
||||
{
|
||||
text: "Using Email",
|
||||
valueKey: "usingEmail",
|
||||
},
|
||||
{
|
||||
text: "Using LDAP",
|
||||
valueKey: "usingLdap",
|
||||
},
|
||||
{
|
||||
text: "API Tokens",
|
||||
valueKey: "apiTokens",
|
||||
},
|
||||
{
|
||||
text: "Users",
|
||||
valueKey: "users",
|
||||
},
|
||||
{
|
||||
text: "Recipes",
|
||||
valueKey: "recipes",
|
||||
},
|
||||
{
|
||||
text: "Groups",
|
||||
valueKey: "groups",
|
||||
},
|
||||
{
|
||||
text: "Shopping Lists",
|
||||
valueKey: "shoppingLists",
|
||||
},
|
||||
{
|
||||
text: "Cookbooks",
|
||||
valueKey: "cookbooks",
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
data,
|
||||
state,
|
||||
analyticsData,
|
||||
getValue,
|
||||
};
|
||||
},
|
||||
head() {
|
||||
return {
|
||||
title: "Analytics",
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<v-container fluid class="narrow-container">
|
||||
<!-- Image -->
|
||||
<BasePageTitle divider>
|
||||
<template #header>
|
||||
<v-img max-height="200" max-width="150" :src="require('~/static/svgs/admin-site-settings.svg')"></v-img>
|
||||
|
@ -7,6 +8,7 @@
|
|||
<template #title> {{ $t("settings.site-settings") }} </template>
|
||||
</BasePageTitle>
|
||||
|
||||
<!-- Bug Report -->
|
||||
<BaseDialog v-model="bugReportDialog" :title="$t('settings.bug-report')" :width="800" :icon="$globals.icons.github">
|
||||
<v-card-text>
|
||||
<div class="pb-4">
|
||||
|
@ -27,7 +29,6 @@
|
|||
<BaseButton
|
||||
color="info"
|
||||
@click="
|
||||
dockerValidate();
|
||||
bugReportDialog = true;
|
||||
"
|
||||
>
|
||||
|
@ -36,6 +37,7 @@
|
|||
</BaseButton>
|
||||
</div>
|
||||
|
||||
<!-- Configuration -->
|
||||
<section>
|
||||
<BaseCardSectionTitle class="pb-0" :icon="$globals.icons.cog" :title="$tc('settings.configuration')"> </BaseCardSectionTitle>
|
||||
<v-card class="mb-4">
|
||||
|
@ -60,40 +62,7 @@
|
|||
</v-card>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<BaseCardSectionTitle class="pt-2" :icon="$globals.icons.docker" :title="$tc('settings.docker-volume')" />
|
||||
<v-alert
|
||||
border="left"
|
||||
colored-border
|
||||
:type="docker.state === DockerVolumeState.Error ? 'error' : 'info'"
|
||||
:icon="$globals.icons.docker"
|
||||
elevation="2"
|
||||
:loading="docker.loading"
|
||||
>
|
||||
<div class="d-flex align-center font-weight-medium">
|
||||
{{ $t('settings.docker-volume') }}
|
||||
<HelpIcon small class="my-n3">
|
||||
{{ $t('settings.docker-volume-help') }}
|
||||
</HelpIcon>
|
||||
</div>
|
||||
<div>
|
||||
<template v-if="docker.state === DockerVolumeState.Error"> {{ $t('settings.volumes-are-misconfigured') }} </template>
|
||||
<template v-else-if="docker.state === DockerVolumeState.Success">
|
||||
{{ $t('settings.volumes-are-configured-correctly') }}
|
||||
</template>
|
||||
<template v-else-if="docker.state === DockerVolumeState.Unknown">
|
||||
{{ $t('settings.status-unknown-try-running-a-validation') }}
|
||||
</template>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<BaseButton color="info" :loading="docker.loading" @click="dockerValidate">
|
||||
<template #icon> {{ $globals.icons.checkboxMarkedCircle }} </template>
|
||||
{{ $t('settings.validate') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</v-alert>
|
||||
</section>
|
||||
|
||||
<!-- Email -->
|
||||
<section>
|
||||
<BaseCardSectionTitle class="pt-2" :icon="$globals.icons.email" :title="$tc('user.email')" />
|
||||
<v-alert border="left" colored-border :type="appConfig.emailReady ? 'success' : 'error'" elevation="2">
|
||||
|
@ -130,40 +99,47 @@
|
|||
<section class="mt-4">
|
||||
<BaseCardSectionTitle class="pb-0" :icon="$globals.icons.cog" :title="$tc('settings.general-about')"> </BaseCardSectionTitle>
|
||||
<v-card class="mb-4">
|
||||
<template v-for="(property, idx) in appInfo">
|
||||
<v-list-item :key="property.name">
|
||||
<v-list-item-icon>
|
||||
<v-icon> {{ property.icon || $globals.icons.user }} </v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
<div>{{ property.name }}</div>
|
||||
</v-list-item-title>
|
||||
<template v-if="property.slot === 'recipe-scraper'">
|
||||
<v-list-item-subtitle>
|
||||
<a
|
||||
target="_blank"
|
||||
:href="`https://github.com/hhursev/recipe-scrapers/releases/tag/${property.value}`"
|
||||
>
|
||||
<template v-if="appInfo && appInfo.length">
|
||||
<template v-for="(property, idx) in appInfo">
|
||||
<v-list-item :key="property.name">
|
||||
<v-list-item-icon>
|
||||
<v-icon> {{ property.icon || $globals.icons.user }} </v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
<div>{{ property.name }}</div>
|
||||
</v-list-item-title>
|
||||
<template v-if="property.slot === 'recipe-scraper'">
|
||||
<v-list-item-subtitle>
|
||||
<a
|
||||
target="_blank"
|
||||
:href="`https://github.com/hhursev/recipe-scrapers/releases/tag/${property.value}`"
|
||||
>
|
||||
{{ property.value }}
|
||||
</a>
|
||||
</v-list-item-subtitle>
|
||||
</template>
|
||||
<template v-else-if="property.slot === 'build'">
|
||||
<v-list-item-subtitle>
|
||||
<a target="_blank" :href="`https://github.com/hay-kot/mealie/commit/${property.value}`">
|
||||
{{ property.value }}
|
||||
</a>
|
||||
</v-list-item-subtitle>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-list-item-subtitle>
|
||||
{{ property.value }}
|
||||
</a>
|
||||
</v-list-item-subtitle>
|
||||
</template>
|
||||
<template v-else-if="property.slot === 'build'">
|
||||
<v-list-item-subtitle>
|
||||
<a target="_blank" :href="`https://github.com/hay-kot/mealie/commit/${property.value}`">
|
||||
{{ property.value }}
|
||||
</a>
|
||||
</v-list-item-subtitle>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-list-item-subtitle>
|
||||
{{ property.value }}
|
||||
</v-list-item-subtitle>
|
||||
</template>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-divider v-if="appInfo && idx !== appInfo.length - 1" :key="`divider-${property.name}`"></v-divider>
|
||||
</v-list-item-subtitle>
|
||||
</template>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-divider v-if="appInfo && idx !== appInfo.length - 1" :key="`divider-${property.name}`"></v-divider>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="mb-3 text-center">
|
||||
<AppLoader :waiting-text="$tc('general.loading')" />
|
||||
</div>
|
||||
</template>
|
||||
</v-card>
|
||||
</section>
|
||||
|
@ -186,6 +162,7 @@ import { useAdminApi, useUserApi } from "~/composables/api";
|
|||
import { validators } from "~/composables/use-validators";
|
||||
import { useAsyncKey } from "~/composables/use-utils";
|
||||
import { CheckAppConfig } from "~/lib/api/types/admin";
|
||||
import AppLoader from "~/components/global/AppLoader.vue";
|
||||
|
||||
enum DockerVolumeState {
|
||||
Unknown = "unknown",
|
||||
|
@ -208,294 +185,230 @@ interface CheckApp extends CheckAppConfig {
|
|||
}
|
||||
|
||||
export default defineComponent({
|
||||
layout: "admin",
|
||||
setup() {
|
||||
// ==========================================================
|
||||
// Docker Volume Validation
|
||||
const docker = reactive({
|
||||
loading: false,
|
||||
state: DockerVolumeState.Unknown,
|
||||
});
|
||||
|
||||
async function dockerValidate() {
|
||||
docker.loading = true;
|
||||
|
||||
// Do API Check
|
||||
const { data } = await adminApi.about.checkDocker();
|
||||
if (data == null) {
|
||||
docker.state = DockerVolumeState.Error;
|
||||
return;
|
||||
}
|
||||
|
||||
// Get File Contents
|
||||
const { data: fileContents } = await adminApi.about.getDockerValidateFileContents();
|
||||
|
||||
if (data.text === fileContents) {
|
||||
docker.state = DockerVolumeState.Success;
|
||||
} else {
|
||||
docker.state = DockerVolumeState.Error;
|
||||
}
|
||||
|
||||
docker.loading = false;
|
||||
}
|
||||
|
||||
const state = reactive({
|
||||
loading: false,
|
||||
address: "",
|
||||
success: false,
|
||||
error: "",
|
||||
tested: false,
|
||||
});
|
||||
|
||||
const appConfig = ref<CheckApp>({
|
||||
emailReady: true,
|
||||
baseUrlSet: true,
|
||||
isSiteSecure: true,
|
||||
isUpToDate: false,
|
||||
ldapReady: false,
|
||||
});
|
||||
|
||||
function isLocalHostOrHttps() {
|
||||
return window.location.hostname === "localhost" || window.location.protocol === "https:";
|
||||
}
|
||||
|
||||
const api = useUserApi();
|
||||
const adminApi = useAdminApi();
|
||||
|
||||
onMounted(async () => {
|
||||
const { data } = await adminApi.about.checkApp();
|
||||
|
||||
if (data) {
|
||||
appConfig.value = { ...data, isSiteSecure: false };
|
||||
}
|
||||
|
||||
appConfig.value.isSiteSecure = isLocalHostOrHttps();
|
||||
});
|
||||
|
||||
const simpleChecks = computed<SimpleCheck[]>(() => {
|
||||
const goodIcon = $globals.icons.checkboxMarkedCircle;
|
||||
const badIcon = $globals.icons.alert;
|
||||
const warningIcon = $globals.icons.alertCircle;
|
||||
|
||||
const goodColor = "success";
|
||||
const badColor = "error";
|
||||
const warningColor = "warning";
|
||||
|
||||
|
||||
const data: SimpleCheck[] = [
|
||||
{
|
||||
id: "application-version",
|
||||
text: i18n.t("settings.application-version"),
|
||||
status: appConfig.value.isUpToDate,
|
||||
errorText: i18n.t("settings.application-version-error-text", [rawAppInfo.value.version, rawAppInfo.value.versionLatest]),
|
||||
successText: i18n.t("settings.mealie-is-up-to-date"),
|
||||
color: appConfig.value.isUpToDate ? goodColor : warningColor,
|
||||
icon: appConfig.value.isUpToDate ? goodIcon : warningIcon,
|
||||
},
|
||||
{
|
||||
id: "secure-site",
|
||||
text: i18n.t("settings.secure-site"),
|
||||
status: appConfig.value.isSiteSecure,
|
||||
errorText: i18n.t("settings.secure-site-error-text"),
|
||||
successText: i18n.t("settings.secure-site-success-text"),
|
||||
color: appConfig.value.isSiteSecure ? goodColor : badColor,
|
||||
icon: appConfig.value.isSiteSecure ? goodIcon : badIcon,
|
||||
},
|
||||
{
|
||||
id: "server-side-base-url",
|
||||
text: i18n.t("settings.server-side-base-url"),
|
||||
status: appConfig.value.baseUrlSet,
|
||||
errorText:
|
||||
i18n.t("settings.server-side-base-url-error-text"),
|
||||
successText: i18n.t("settings.server-side-base-url-success-text"),
|
||||
color: appConfig.value.baseUrlSet ? goodColor : badColor,
|
||||
icon: appConfig.value.baseUrlSet ? goodIcon : badIcon,
|
||||
},
|
||||
{
|
||||
id: "ldap-ready",
|
||||
text: i18n.t("settings.ldap-ready"),
|
||||
status: appConfig.value.ldapReady,
|
||||
errorText:
|
||||
i18n.t("settings.ldap-ready-error-text"),
|
||||
successText: i18n.t("settings.ldap-ready-success-text"),
|
||||
color: appConfig.value.ldapReady ? goodColor : warningColor,
|
||||
icon: appConfig.value.ldapReady ? goodIcon : warningIcon,
|
||||
},
|
||||
];
|
||||
|
||||
return data;
|
||||
});
|
||||
|
||||
async function testEmail() {
|
||||
state.loading = true;
|
||||
state.tested = false;
|
||||
const { data } = await api.email.test({ email: state.address });
|
||||
|
||||
if (data) {
|
||||
if (data.success) {
|
||||
state.success = true;
|
||||
} else {
|
||||
state.error = data.error ?? "";
|
||||
state.success = false;
|
||||
components: { AppLoader },
|
||||
layout: "admin",
|
||||
setup() {
|
||||
const state = reactive({
|
||||
loading: false,
|
||||
address: "",
|
||||
success: false,
|
||||
error: "",
|
||||
tested: false,
|
||||
});
|
||||
const appConfig = ref<CheckApp>({
|
||||
emailReady: true,
|
||||
baseUrlSet: true,
|
||||
isSiteSecure: true,
|
||||
isUpToDate: false,
|
||||
ldapReady: false,
|
||||
});
|
||||
function isLocalHostOrHttps() {
|
||||
return window.location.hostname === "localhost" || window.location.protocol === "https:";
|
||||
}
|
||||
}
|
||||
state.loading = false;
|
||||
state.tested = true;
|
||||
const api = useUserApi();
|
||||
const adminApi = useAdminApi();
|
||||
onMounted(async () => {
|
||||
const { data } = await adminApi.about.checkApp();
|
||||
if (data) {
|
||||
appConfig.value = { ...data, isSiteSecure: false };
|
||||
}
|
||||
appConfig.value.isSiteSecure = isLocalHostOrHttps();
|
||||
});
|
||||
const simpleChecks = computed<SimpleCheck[]>(() => {
|
||||
const goodIcon = $globals.icons.checkboxMarkedCircle;
|
||||
const badIcon = $globals.icons.alert;
|
||||
const warningIcon = $globals.icons.alertCircle;
|
||||
const goodColor = "success";
|
||||
const badColor = "error";
|
||||
const warningColor = "warning";
|
||||
const data: SimpleCheck[] = [
|
||||
{
|
||||
id: "application-version",
|
||||
text: i18n.t("settings.application-version"),
|
||||
status: appConfig.value.isUpToDate,
|
||||
errorText: i18n.t("settings.application-version-error-text", [rawAppInfo.value.version, rawAppInfo.value.versionLatest]),
|
||||
successText: i18n.t("settings.mealie-is-up-to-date"),
|
||||
color: appConfig.value.isUpToDate ? goodColor : warningColor,
|
||||
icon: appConfig.value.isUpToDate ? goodIcon : warningIcon,
|
||||
},
|
||||
{
|
||||
id: "secure-site",
|
||||
text: i18n.t("settings.secure-site"),
|
||||
status: appConfig.value.isSiteSecure,
|
||||
errorText: i18n.t("settings.secure-site-error-text"),
|
||||
successText: i18n.t("settings.secure-site-success-text"),
|
||||
color: appConfig.value.isSiteSecure ? goodColor : badColor,
|
||||
icon: appConfig.value.isSiteSecure ? goodIcon : badIcon,
|
||||
},
|
||||
{
|
||||
id: "server-side-base-url",
|
||||
text: i18n.t("settings.server-side-base-url"),
|
||||
status: appConfig.value.baseUrlSet,
|
||||
errorText: i18n.t("settings.server-side-base-url-error-text"),
|
||||
successText: i18n.t("settings.server-side-base-url-success-text"),
|
||||
color: appConfig.value.baseUrlSet ? goodColor : badColor,
|
||||
icon: appConfig.value.baseUrlSet ? goodIcon : badIcon,
|
||||
},
|
||||
{
|
||||
id: "ldap-ready",
|
||||
text: i18n.t("settings.ldap-ready"),
|
||||
status: appConfig.value.ldapReady,
|
||||
errorText: i18n.t("settings.ldap-ready-error-text"),
|
||||
successText: i18n.t("settings.ldap-ready-success-text"),
|
||||
color: appConfig.value.ldapReady ? goodColor : warningColor,
|
||||
icon: appConfig.value.ldapReady ? goodIcon : warningIcon,
|
||||
},
|
||||
];
|
||||
return data;
|
||||
});
|
||||
async function testEmail() {
|
||||
state.loading = true;
|
||||
state.tested = false;
|
||||
const { data } = await api.email.test({ email: state.address });
|
||||
if (data) {
|
||||
if (data.success) {
|
||||
state.success = true;
|
||||
}
|
||||
else {
|
||||
state.error = data.error ?? "";
|
||||
state.success = false;
|
||||
}
|
||||
}
|
||||
state.loading = false;
|
||||
state.tested = true;
|
||||
}
|
||||
const validEmail = computed(() => {
|
||||
if (state.address === "") {
|
||||
return false;
|
||||
}
|
||||
const valid = validators.email(state.address);
|
||||
// Explicit bool check because validators.email sometimes returns a string
|
||||
if (valid === true) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
// ============================================================
|
||||
// General About Info
|
||||
const { $globals, i18n } = useContext();
|
||||
const rawAppInfo = ref({
|
||||
version: "null",
|
||||
versionLatest: "null",
|
||||
});
|
||||
function getAppInfo() {
|
||||
const statistics = useAsync(async () => {
|
||||
const { data } = await adminApi.about.about();
|
||||
if (data) {
|
||||
rawAppInfo.value.version = data.version;
|
||||
rawAppInfo.value.versionLatest = data.versionLatest;
|
||||
const prettyInfo = [
|
||||
{
|
||||
name: i18n.t("about.version"),
|
||||
icon: $globals.icons.information,
|
||||
value: data.version,
|
||||
},
|
||||
{
|
||||
slot: "build",
|
||||
name: i18n.t("settings.build"),
|
||||
icon: $globals.icons.information,
|
||||
value: data.buildId,
|
||||
},
|
||||
{
|
||||
name: i18n.t("about.application-mode"),
|
||||
icon: $globals.icons.devTo,
|
||||
value: data.production ? i18n.t("about.production") : i18n.t("about.development"),
|
||||
},
|
||||
{
|
||||
name: i18n.t("about.demo-status"),
|
||||
icon: $globals.icons.testTube,
|
||||
value: data.demoStatus ? i18n.t("about.demo") : i18n.t("about.not-demo"),
|
||||
},
|
||||
{
|
||||
name: i18n.t("about.api-port"),
|
||||
icon: $globals.icons.api,
|
||||
value: data.apiPort,
|
||||
},
|
||||
{
|
||||
name: i18n.t("about.api-docs"),
|
||||
icon: $globals.icons.file,
|
||||
value: data.apiDocs ? i18n.t("general.enabled") : i18n.t("general.disabled"),
|
||||
},
|
||||
{
|
||||
name: i18n.t("about.database-type"),
|
||||
icon: $globals.icons.database,
|
||||
value: data.dbType,
|
||||
},
|
||||
{
|
||||
name: i18n.t("about.database-url"),
|
||||
icon: $globals.icons.database,
|
||||
value: data.dbUrl,
|
||||
},
|
||||
{
|
||||
name: i18n.t("about.default-group"),
|
||||
icon: $globals.icons.group,
|
||||
value: data.defaultGroup,
|
||||
},
|
||||
{
|
||||
slot: "recipe-scraper",
|
||||
name: i18n.t("settings.recipe-scraper-version"),
|
||||
icon: $globals.icons.primary,
|
||||
value: data.recipeScraperVersion,
|
||||
},
|
||||
];
|
||||
return prettyInfo;
|
||||
}
|
||||
return data;
|
||||
}, useAsyncKey());
|
||||
return statistics;
|
||||
}
|
||||
const appInfo = getAppInfo();
|
||||
const bugReportDialog = ref(false);
|
||||
const bugReportText = computed(() => {
|
||||
const ignore = {
|
||||
[i18n.tc("about.database-url")]: true,
|
||||
[i18n.tc("about.default-group")]: true,
|
||||
};
|
||||
let text = "**Details**\n";
|
||||
appInfo.value?.forEach((item) => {
|
||||
if (ignore[item.name as string]) {
|
||||
return;
|
||||
}
|
||||
text += `${item.name as string}: ${item.value as string}\n`;
|
||||
});
|
||||
const ignoreChecks: {
|
||||
[key: string]: boolean;
|
||||
} = {
|
||||
"application-version": true,
|
||||
};
|
||||
text += "\n**Checks**\n";
|
||||
simpleChecks.value.forEach((item) => {
|
||||
if (ignoreChecks[item.id]) {
|
||||
return;
|
||||
}
|
||||
const status = item.status ? i18n.tc("general.yes") : i18n.tc("general.no");
|
||||
text += `${item.text.toString()}: ${status}\n`;
|
||||
});
|
||||
text += `${i18n.tc("settings.email-configured")}: ${appConfig.value.emailReady ? i18n.tc("general.yes") : i18n.tc("general.no")}\n`;
|
||||
return text;
|
||||
});
|
||||
return {
|
||||
bugReportDialog,
|
||||
bugReportText,
|
||||
DockerVolumeState,
|
||||
simpleChecks,
|
||||
appConfig,
|
||||
validEmail,
|
||||
validators,
|
||||
...toRefs(state),
|
||||
testEmail,
|
||||
appInfo,
|
||||
};
|
||||
},
|
||||
head() {
|
||||
return {
|
||||
title: this.$t("settings.site-settings") as string,
|
||||
};
|
||||
}
|
||||
|
||||
const validEmail = computed(() => {
|
||||
if (state.address === "") {
|
||||
return false;
|
||||
}
|
||||
const valid = validators.email(state.address);
|
||||
|
||||
// Explicit bool check because validators.email sometimes returns a string
|
||||
if (valid === true) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// General About Info
|
||||
|
||||
const { $globals, i18n } = useContext();
|
||||
|
||||
const rawAppInfo = ref({
|
||||
version: "null",
|
||||
versionLatest: "null",
|
||||
});
|
||||
|
||||
function getAppInfo() {
|
||||
const statistics = useAsync(async () => {
|
||||
const { data } = await adminApi.about.about();
|
||||
|
||||
if (data) {
|
||||
rawAppInfo.value.version = data.version;
|
||||
rawAppInfo.value.versionLatest = data.versionLatest;
|
||||
|
||||
const prettyInfo = [
|
||||
{
|
||||
name: i18n.t("about.version"),
|
||||
icon: $globals.icons.information,
|
||||
value: data.version,
|
||||
},
|
||||
{
|
||||
slot: "build",
|
||||
name: i18n.t("settings.build"),
|
||||
icon: $globals.icons.information,
|
||||
value: data.buildId,
|
||||
},
|
||||
{
|
||||
name: i18n.t("about.application-mode"),
|
||||
icon: $globals.icons.devTo,
|
||||
value: data.production ? i18n.t("about.production") : i18n.t("about.development"),
|
||||
},
|
||||
{
|
||||
name: i18n.t("about.demo-status"),
|
||||
icon: $globals.icons.testTube,
|
||||
value: data.demoStatus ? i18n.t("about.demo") : i18n.t("about.not-demo"),
|
||||
},
|
||||
{
|
||||
name: i18n.t("about.api-port"),
|
||||
icon: $globals.icons.api,
|
||||
value: data.apiPort,
|
||||
},
|
||||
{
|
||||
name: i18n.t("about.api-docs"),
|
||||
icon: $globals.icons.file,
|
||||
value: data.apiDocs ? i18n.t("general.enabled") : i18n.t("general.disabled"),
|
||||
},
|
||||
{
|
||||
name: i18n.t("about.database-type"),
|
||||
icon: $globals.icons.database,
|
||||
value: data.dbType,
|
||||
},
|
||||
{
|
||||
name: i18n.t("about.database-url"),
|
||||
icon: $globals.icons.database,
|
||||
value: data.dbUrl,
|
||||
},
|
||||
{
|
||||
name: i18n.t("about.default-group"),
|
||||
icon: $globals.icons.group,
|
||||
value: data.defaultGroup,
|
||||
},
|
||||
{
|
||||
slot: "recipe-scraper",
|
||||
name: i18n.t("settings.recipe-scraper-version"),
|
||||
icon: $globals.icons.primary,
|
||||
value: data.recipeScraperVersion,
|
||||
},
|
||||
];
|
||||
|
||||
return prettyInfo;
|
||||
}
|
||||
|
||||
return data;
|
||||
}, useAsyncKey());
|
||||
|
||||
return statistics;
|
||||
}
|
||||
|
||||
const appInfo = getAppInfo();
|
||||
|
||||
const bugReportDialog = ref(false);
|
||||
|
||||
const bugReportText = computed(() => {
|
||||
const ignore = {
|
||||
[i18n.tc("about.database-url")]: true,
|
||||
[i18n.tc("about.default-group")]: true,
|
||||
};
|
||||
let text = "**Details**\n";
|
||||
|
||||
appInfo.value?.forEach((item) => {
|
||||
if (ignore[item.name as string]) {
|
||||
return;
|
||||
}
|
||||
text += `${item.name as string}: ${item.value as string}\n`;
|
||||
});
|
||||
|
||||
const ignoreChecks: { [key: string]: boolean } = {
|
||||
"application-version": true,
|
||||
};
|
||||
|
||||
text += "\n**Checks**\n";
|
||||
|
||||
simpleChecks.value.forEach((item) => {
|
||||
if (ignoreChecks[item.id]) {
|
||||
return;
|
||||
}
|
||||
const status = item.status ? i18n.tc("general.yes") : i18n.tc("general.no");
|
||||
text += `${item.text.toString()}: ${status}\n`;
|
||||
});
|
||||
|
||||
text += `${i18n.tc("settings.email-configured")}: ${appConfig.value.emailReady ? i18n.tc("general.yes") : i18n.tc("general.no")}\n`;
|
||||
text += `${i18n.tc("settings.docker-volume")}: ${docker.state}`;
|
||||
|
||||
return text;
|
||||
});
|
||||
|
||||
return {
|
||||
bugReportDialog,
|
||||
bugReportText,
|
||||
DockerVolumeState,
|
||||
docker,
|
||||
dockerValidate,
|
||||
simpleChecks,
|
||||
appConfig,
|
||||
validEmail,
|
||||
validators,
|
||||
...toRefs(state),
|
||||
testEmail,
|
||||
appInfo,
|
||||
};
|
||||
},
|
||||
head() {
|
||||
return {
|
||||
title: this.$t("settings.site-settings") as string,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, ref, useAsync, useContext, useMeta, useRoute, useRouter } from "@nuxtjs/composition-api";
|
||||
import { whenever } from "@vueuse/core";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import { useAsyncKey } from "~/composables/use-utils";
|
||||
import RecipePage from "~/components/Domain/Recipe/RecipePage/RecipePage.vue";
|
||||
import { usePublicExploreApi } from "~/composables/api/api-client";
|
||||
import { useRecipe } from "~/composables/recipes";
|
||||
|
@ -15,14 +17,13 @@ import { Recipe } from "~/lib/api/types/recipe";
|
|||
export default defineComponent({
|
||||
components: { RecipePage },
|
||||
setup() {
|
||||
const { $auth } = useContext();
|
||||
const { $auth } = useContext();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
const { title } = useMeta();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const slug = route.value.params.slug;
|
||||
|
||||
const { title } = useMeta();
|
||||
|
||||
let recipe = ref<Recipe | null>(null);
|
||||
if (isOwnGroup.value) {
|
||||
const { recipe: data } = useRecipe(slug);
|
||||
|
@ -32,28 +33,28 @@ export default defineComponent({
|
|||
const api = usePublicExploreApi(groupSlug.value);
|
||||
recipe = useAsync(async () => {
|
||||
const { data, error } = await api.explore.recipes.getOne(slug);
|
||||
|
||||
if (error) {
|
||||
console.error("error loading recipe -> ", error);
|
||||
router.push(`/g/${groupSlug.value}`);
|
||||
}
|
||||
|
||||
return data;
|
||||
})
|
||||
}, useAsyncKey())
|
||||
}
|
||||
|
||||
title.value = recipe.value?.name || "";
|
||||
whenever(
|
||||
() => recipe.value,
|
||||
() => {
|
||||
if (recipe.value) {
|
||||
title.value = recipe.value.name;
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
recipe,
|
||||
};
|
||||
},
|
||||
head() {
|
||||
if (this.recipe) {
|
||||
return {
|
||||
title: this.recipe.name
|
||||
}
|
||||
}
|
||||
}
|
||||
head: {},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -103,7 +103,11 @@ export default defineComponent({
|
|||
if (refreshTags) {
|
||||
tags.actions.refresh();
|
||||
}
|
||||
router.push(`/g/${groupSlug.value}/r/${response.data}?edit=${edit.toString()}`);
|
||||
|
||||
// we clear the query params first so if the user hits back, they don't re-import the recipe
|
||||
router.replace({ query: {} }).then(
|
||||
() => router.push(`/g/${groupSlug.value}/r/${response.data}?edit=${edit.toString()}`)
|
||||
);
|
||||
}
|
||||
|
||||
const recipeUrl = computed({
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
</template>
|
||||
<template #title> {{ $t('data-pages.data-management') }} </template>
|
||||
{{ $t('data-pages.data-management-description') }}
|
||||
<BannerExperimental class="mt-5"></BannerExperimental>
|
||||
<template #content>
|
||||
<div>
|
||||
<BaseOverflowButton
|
||||
|
|
|
@ -121,7 +121,7 @@
|
|||
<template #icon>
|
||||
{{ $globals.icons.database }}
|
||||
</template>
|
||||
{{ $t('general.import') }}}
|
||||
{{ $t('general.import') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
color="info"
|
||||
|
|
|
@ -24,8 +24,8 @@
|
|||
</v-date-picker>
|
||||
</v-menu>
|
||||
|
||||
<div class="d-flex align-center justify-space-between mb-2">
|
||||
<v-tabs>
|
||||
<div class="d-flex flex-wrap align-center justify-space-between mb-2">
|
||||
<v-tabs style="width: fit-content;">
|
||||
<v-tab :to="`/group/mealplan/planner/view`">{{ $t('meal-plan.meal-planner') }}</v-tab>
|
||||
<v-tab :to="`/group/mealplan/planner/edit`">{{ $t('general.edit') }}</v-tab>
|
||||
</v-tabs>
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
{{ $d(Date.parse(item.timestamp), "short") }}
|
||||
</template>
|
||||
<template #expanded-item="{ headers, item }">
|
||||
<td class="pa-6" :colspan="headers.length">{{ item.exception }}</td>
|
||||
<td v-if="item.exception" class="pa-6" :colspan="headers.length">{{ item.exception }}</td>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-container>
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
<p>{{ $t('profile.account-summary-description') }}</p>
|
||||
</div>
|
||||
<v-row tag="section">
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-col cols="12" sm="12" md="12">
|
||||
<v-card outlined>
|
||||
<v-card-title class="headline pb-0"> {{ $t('profile.group-statistics') }} </v-card-title>
|
||||
<v-card-text class="py-0">
|
||||
|
@ -75,22 +75,6 @@
|
|||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6" class="d-flex align-strart">
|
||||
<v-card outlined>
|
||||
<v-card-title class="headline pb-0"> {{ $t('profile.storage-capacity') }} </v-card-title>
|
||||
<v-card-text class="py-0">
|
||||
{{ $t('profile.storage-capacity-description') }}
|
||||
<strong> {{ $t('general.this-feature-is-currently-inactive') }}</strong>
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
<v-progress-linear :value="storageUsedPercentage" color="accent" class="rounded" height="30">
|
||||
<template #default>
|
||||
<strong> {{ storageText }} </strong>
|
||||
</template>
|
||||
</v-progress-linear>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</section>
|
||||
<v-divider class="my-7"></v-divider>
|
||||
|
@ -344,33 +328,8 @@ export default defineComponent({
|
|||
return statsTo.value[key] ?? "unknown";
|
||||
}
|
||||
|
||||
const storage = useAsync(async () => {
|
||||
const { data } = await api.groups.storage();
|
||||
|
||||
if (data) {
|
||||
return data;
|
||||
}
|
||||
}, useAsyncKey());
|
||||
|
||||
const storageUsedPercentage = computed(() => {
|
||||
if (!storage.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (storage.value?.usedStorageBytes / storage.value?.totalStorageBytes) * 100 ?? 0;
|
||||
});
|
||||
|
||||
const storageText = computed(() => {
|
||||
if (!storage.value) {
|
||||
return "Loading...";
|
||||
}
|
||||
return `${storage.value.usedStorageStr} / ${storage.value.totalStorageStr}`;
|
||||
});
|
||||
|
||||
return {
|
||||
groupSlug,
|
||||
storageText,
|
||||
storageUsedPercentage,
|
||||
getStatsTitle,
|
||||
getStatsIcon,
|
||||
getStatsTo,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue