mirror of
https://github.com/unraid/api.git
synced 2026-01-06 08:39:54 -06:00
move into web for api repo merging
This commit is contained in:
18
web/components/UserProfile/Beta.vue
Normal file
18
web/components/UserProfile/Beta.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
export interface Props {
|
||||
colorClasses?: string
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
colorClasses: 'text-grey-mid border-grey-mid',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
class="text-10px uppercase py-4px px-6px border-2 rounded-full"
|
||||
:class="colorClasses"
|
||||
>
|
||||
{{ 'Beta' }}
|
||||
</span>
|
||||
</template>
|
||||
340
web/components/UserProfile/CallbackFeedback.vue
Normal file
340
web/components/UserProfile/CallbackFeedback.vue
Normal file
@@ -0,0 +1,340 @@
|
||||
<script lang="ts" setup>
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { ClipboardIcon, CogIcon, InformationCircleIcon } from '@heroicons/vue/24/solid';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import '~/assets/main.css';
|
||||
import { PLUGIN_SETTINGS } from '~/helpers/urls';
|
||||
import { useAccountStore } from '~/store/account';
|
||||
import { useCallbackActionsStore } from '~/store/callbackActions';
|
||||
import { useInstallKeyStore } from '~/store/installKey';
|
||||
import { usePromoStore } from '~/store/promo';
|
||||
import { useServerStore } from '~/store/server';
|
||||
|
||||
export interface Props {
|
||||
open?: boolean;
|
||||
t: any;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
open: false,
|
||||
});
|
||||
|
||||
const accountStore = useAccountStore();
|
||||
const callbackActionsStore = useCallbackActionsStore();
|
||||
const installKeyStore = useInstallKeyStore();
|
||||
const promoStore = usePromoStore();
|
||||
const serverStore = useServerStore();
|
||||
|
||||
const {
|
||||
accountAction,
|
||||
accountActionHide,
|
||||
accountActionStatus,
|
||||
accountActionType,
|
||||
} = storeToRefs(accountStore);
|
||||
const {
|
||||
callbackStatus,
|
||||
} = storeToRefs(callbackActionsStore);
|
||||
const {
|
||||
keyActionType,
|
||||
keyUrl,
|
||||
keyInstallStatus,
|
||||
keyType,
|
||||
} = storeToRefs(installKeyStore);
|
||||
const {
|
||||
connectPluginInstalled,
|
||||
registered,
|
||||
authAction,
|
||||
refreshServerStateStatus,
|
||||
username,
|
||||
} = storeToRefs(serverStore);
|
||||
|
||||
/**
|
||||
* Post sign in success state:
|
||||
* If we're on the Connect settings page in the webGUI
|
||||
* the modal should close instead of redirecting to the
|
||||
* settings page.
|
||||
*
|
||||
* @todo figure out the difference between document.location and window.location in relation to the webGUI and webGUI being iframed
|
||||
*/
|
||||
const isSettingsPage = ref<boolean>(document.location.pathname === '/Settings/ManagementAccess');
|
||||
|
||||
const showPromoCta = computed(() => callbackStatus.value === 'success' && !connectPluginInstalled.value);
|
||||
const showSignInCta = computed(() => connectPluginInstalled.value && !registered.value && authAction.value?.name === 'signIn' && accountActionType.value !== 'signIn');
|
||||
|
||||
const heading = computed(() => {
|
||||
switch (callbackStatus.value) {
|
||||
case 'error':
|
||||
return props.t('Error');
|
||||
case 'loading':
|
||||
return props.t('Performing actions');
|
||||
case 'success':
|
||||
return props.t('Success!');
|
||||
}
|
||||
});
|
||||
const subheading = computed(() => {
|
||||
if (callbackStatus.value === 'error') {
|
||||
return props.t('Something went wrong'); /** @todo show actual error messages */
|
||||
}
|
||||
if (callbackStatus.value === 'loading') { return props.t('Please keep this window open while we perform some actions'); }
|
||||
if (callbackStatus.value === 'success') {
|
||||
if (accountActionType.value === 'signIn') { return props.t('You\'re one step closer to enhancing your Unraid experience'); }
|
||||
if (keyActionType.value === 'purchase') { return props.t('Thank you for purchasing an Unraid {0} Key!', [keyType.value]); }
|
||||
if (keyActionType.value === 'replace') { return props.t('Your {0} Key has been replaced!', [keyType.value]); }
|
||||
if (keyActionType.value === 'trialExtend') { return props.t('Your Trial key has been extended!'); }
|
||||
if (keyActionType.value === 'trialStart') { return props.t('Your free Trial key provides all the functionality of a Pro Registration key'); }
|
||||
if (keyActionType.value === 'upgrade') { return props.t('Thank you for upgrading to an Unraid {0} Key!', [keyType.value]); }
|
||||
return '';
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
const closeText = computed(() => {
|
||||
const txt = !connectPluginInstalled.value ? props.t('No thanks') : props.t('Close');
|
||||
return refreshServerStateStatus.value === 'done' ? txt : props.t('Reload');
|
||||
});
|
||||
const close = () => {
|
||||
if (callbackStatus.value === 'loading') { return console.debug('[close] not allowed'); }
|
||||
return refreshServerStateStatus.value === 'done'
|
||||
? callbackActionsStore.setCallbackStatus('ready')
|
||||
: window.location.reload();
|
||||
};
|
||||
|
||||
const promoClick = () => {
|
||||
promoStore.openOnNextLoad();
|
||||
close();
|
||||
};
|
||||
|
||||
const { copy, copied, isSupported } = useClipboard({ source: keyUrl.value });
|
||||
|
||||
const keyInstallStatusCopy = computed((): { text: string; } => {
|
||||
let txt1 = props.t('Installing');
|
||||
let txt2 = props.t('Installed');
|
||||
let txt3 = props.t('Install');
|
||||
switch (keyInstallStatus.value) {
|
||||
case 'ready':
|
||||
return {
|
||||
text: props.t('Ready to Install Key'),
|
||||
};
|
||||
case 'installing':
|
||||
if (keyActionType.value === 'trialExtend') { txt1 = props.t('Installing Extended Trial'); }
|
||||
if (keyActionType.value === 'recover') { txt1 = props.t('Installing Recovered'); }
|
||||
if (keyActionType.value === 'replace') { txt1 = props.t('Installing Replaced'); }
|
||||
return {
|
||||
text: props.t('{0} {1} Key…', [txt1, keyType.value]),
|
||||
};
|
||||
case 'success':
|
||||
if (keyActionType.value === 'trialExtend') { txt2 = props.t('Extension Installed'); }
|
||||
if (keyActionType.value === 'recover') { txt2 = props.t('Recovered'); }
|
||||
if (keyActionType.value === 'replace') { txt2 = props.t('Replaced'); }
|
||||
return {
|
||||
text: props.t('{1} Key {0} Successfully', [txt2, keyType.value]),
|
||||
};
|
||||
case 'failed':
|
||||
if (keyActionType.value === 'trialExtend') { txt3 = props.t('Install Extended'); }
|
||||
if (keyActionType.value === 'recover') { txt3 = props.t('Install Recovered'); }
|
||||
if (keyActionType.value === 'replace') { txt3 = props.t('Install Replaced'); }
|
||||
return {
|
||||
text: props.t('Failed to {0} {1} Key', [txt3, keyType.value]),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const accountActionStatusCopy = computed((): { text: string; } => {
|
||||
switch (accountActionStatus.value) {
|
||||
case 'ready':
|
||||
return {
|
||||
text: props.t('Ready to update Connect account configuration'),
|
||||
};
|
||||
case 'updating':
|
||||
return {
|
||||
text: accountAction.value?.type === 'signIn'
|
||||
? props.t('Signing in {0}…', [accountAction.value.user?.preferred_username])
|
||||
: props.t('Signing out {0}…', [username.value]),
|
||||
};
|
||||
case 'success':
|
||||
return {
|
||||
text: accountAction.value?.type === 'signIn'
|
||||
? props.t('{0} Signed In Successfully', [accountAction.value.user?.preferred_username])
|
||||
: props.t('{0} Signed Out Successfully', [username.value]),
|
||||
};
|
||||
case 'failed':
|
||||
return {
|
||||
text: accountAction.value?.type === 'signIn'
|
||||
? props.t('Sign In Failed')
|
||||
: props.t('Sign Out Failed'),
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
:t="t"
|
||||
:title="heading"
|
||||
:description="subheading"
|
||||
:open="open"
|
||||
max-width="max-w-640px"
|
||||
:error="callbackStatus === 'error'"
|
||||
:success="callbackStatus === 'success'"
|
||||
:show-close-x="callbackStatus !== 'loading'"
|
||||
@close="close"
|
||||
>
|
||||
<template #main>
|
||||
<div
|
||||
v-if="keyInstallStatus !== 'ready' || accountActionStatus !== 'ready'"
|
||||
class="text-center relative w-full flex flex-col justify-center gap-y-16px py-24px sm:py-32px"
|
||||
>
|
||||
<BrandLoading v-if="callbackStatus === 'loading'" class="w-[110px] mx-auto" />
|
||||
|
||||
<UpcCallbackFeedbackStatus
|
||||
v-if="keyInstallStatus !== 'ready'"
|
||||
:success="keyInstallStatus === 'success'"
|
||||
:error="keyInstallStatus === 'failed'"
|
||||
:text="keyInstallStatusCopy.text"
|
||||
>
|
||||
<UpcUptimeExpire
|
||||
v-if="keyType === 'Trial'"
|
||||
:for-expire="true"
|
||||
class="opacity-75 italic mt-4px"
|
||||
:t="t"
|
||||
/>
|
||||
|
||||
<template v-if="keyInstallStatus === 'failed'">
|
||||
<div v-if="isSupported" class="flex justify-center">
|
||||
<BrandButton
|
||||
:icon="ClipboardIcon"
|
||||
:text="copied ? t('Copied') : t('Copy Key URL')"
|
||||
@click="copy(keyUrl)"
|
||||
/>
|
||||
</div>
|
||||
<p v-else>
|
||||
{{ t('Copy your Key URL: {0}', [keyUrl]) }}
|
||||
</p>
|
||||
<p>
|
||||
<a href="/Tools/Registration" class="opacity-75 hover:opacity-100 focus:opacity-100 underline transition">
|
||||
{{ t('Then go to Tools > Registration to manually install it') }}
|
||||
</a>
|
||||
</p>
|
||||
</template>
|
||||
</UpcCallbackFeedbackStatus>
|
||||
|
||||
<UpcCallbackFeedbackStatus
|
||||
v-if="accountActionStatus !== 'ready' && !accountActionHide"
|
||||
:success="accountActionStatus === 'success'"
|
||||
:error="accountActionStatus === 'failed'"
|
||||
:text="accountActionStatusCopy.text"
|
||||
/>
|
||||
|
||||
<UpcCallbackFeedbackStatus
|
||||
v-if="showPromoCta"
|
||||
:icon="InformationCircleIcon"
|
||||
:text="t('Enhance your experience with Unraid Connect')"
|
||||
/>
|
||||
|
||||
<UpcCallbackFeedbackStatus
|
||||
v-if="showSignInCta"
|
||||
:icon="InformationCircleIcon"
|
||||
:text="t('Sign In to utilize Unraid Connect')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div v-if="callbackStatus === 'success'" class="flex flex-row-reverse justify-center gap-16px">
|
||||
<template v-if="connectPluginInstalled && accountActionType === 'signIn'">
|
||||
<BrandButton
|
||||
v-if="isSettingsPage"
|
||||
:icon="CogIcon"
|
||||
:text="t('Configure Connect Features')"
|
||||
class="grow-0"
|
||||
@click="close"
|
||||
/>
|
||||
<BrandButton
|
||||
v-else
|
||||
:href="PLUGIN_SETTINGS.toString()"
|
||||
:icon="CogIcon"
|
||||
:text="t('Configure Connect Features')"
|
||||
class="grow-0"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<BrandButton
|
||||
v-if="showPromoCta"
|
||||
:text="t('Learn More')"
|
||||
@click="promoClick"
|
||||
/>
|
||||
|
||||
<BrandButton
|
||||
v-if="showSignInCta"
|
||||
:disabled="authAction?.disabled"
|
||||
:external="authAction?.external"
|
||||
:icon="authAction?.icon"
|
||||
:text="t(authAction?.text)"
|
||||
@click="authAction?.click"
|
||||
/>
|
||||
|
||||
<BrandButton
|
||||
btn-style="underline"
|
||||
:text="closeText"
|
||||
@click="close"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style lang="postcss">
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
|
||||
.unraid_mark_2,
|
||||
.unraid_mark_4 {
|
||||
animation: mark_2 1.5s ease infinite;
|
||||
}
|
||||
.unraid_mark_3 {
|
||||
animation: mark_3 1.5s ease infinite;
|
||||
}
|
||||
.unraid_mark_6,
|
||||
.unraid_mark_8 {
|
||||
animation: mark_6 1.5s ease infinite;
|
||||
}
|
||||
.unraid_mark_7 {
|
||||
animation: mark_7 1.5s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes mark_2 {
|
||||
50% {
|
||||
transform: translateY(-40px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes mark_3 {
|
||||
50% {
|
||||
transform: translateY(-62px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes mark_6 {
|
||||
50% {
|
||||
transform: translateY(40px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes mark_7 {
|
||||
50% {
|
||||
transform: translateY(62px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
</style>
|
||||
31
web/components/UserProfile/CallbackFeedbackStatus.vue
Normal file
31
web/components/UserProfile/CallbackFeedbackStatus.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { CheckCircleIcon, XCircleIcon } from '@heroicons/vue/24/solid';
|
||||
|
||||
export interface Props {
|
||||
error?: boolean;
|
||||
icon?: typeof CheckCircleIcon;
|
||||
success?: boolean;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
error: false,
|
||||
icon: undefined,
|
||||
success: false,
|
||||
text: undefined,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-auto max-w-[45ch]">
|
||||
<div class="flex items-start justify-center gap-x-8px">
|
||||
<CheckCircleIcon v-if="success" class="fill-green-600 w-28px shrink-0" />
|
||||
<XCircleIcon v-if="error" class="fill-unraid-red w-28px shrink-0" />
|
||||
<component :is="icon" v-if="icon" class="fill-current opacity-75 w-28px shrink-0" />
|
||||
<p class="text-18px">
|
||||
{{ text }}
|
||||
</p>
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
34
web/components/UserProfile/Dropdown.vue
Normal file
34
web/components/UserProfile/Dropdown.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import { TransitionRoot } from '@headlessui/vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
import { useDropdownStore } from '~/store/dropdown';
|
||||
import { useServerStore } from '~/store/server';
|
||||
|
||||
defineProps<{ t: any; }>();
|
||||
|
||||
const dropdownStore = useDropdownStore();
|
||||
const { dropdownVisible } = storeToRefs(dropdownStore);
|
||||
const { connectPluginInstalled, registered, state, stateDataError } = storeToRefs(useServerStore());
|
||||
|
||||
const showDefaultContent = computed(() => !showLaunchpad.value);
|
||||
const showLaunchpad = computed(() => state.value === 'ENOKEYFILE' || ((connectPluginInstalled.value && !registered.value) && !stateDataError.value));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TransitionRoot
|
||||
as="template"
|
||||
:show="dropdownVisible"
|
||||
enter="transition-all duration-200"
|
||||
enter-from="opacity-0 translate-y-[16px]"
|
||||
enter-to="opacity-100"
|
||||
leave="transition-all duration-150"
|
||||
leave-from="opacity-100"
|
||||
leave-to="opacity-0 translate-y-[16px]"
|
||||
>
|
||||
<UpcDropdownWrapper class="DropdownWrapper_blip text-beta absolute z-30 top-full right-0 transition-all">
|
||||
<UpcDropdownContent v-if="showDefaultContent" :t="t" />
|
||||
<UpcDropdownLaunchpad v-else-if="showLaunchpad" :t="t" />
|
||||
</UpcDropdownWrapper>
|
||||
</TransitionRoot>
|
||||
</template>
|
||||
71
web/components/UserProfile/DropdownConnectStatus.vue
Normal file
71
web/components/UserProfile/DropdownConnectStatus.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
import { ExclamationTriangleIcon, CheckCircleIcon } from '@heroicons/vue/24/solid';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
import BrandLoading from '~/components/Brand/Loading.vue';
|
||||
import { useUnraidApiStore } from '~/store/unraidApi';
|
||||
|
||||
const props = defineProps<{ t: any; }>();
|
||||
|
||||
const unraidApiStore = useUnraidApiStore();
|
||||
const { unraidApiStatus, unraidApiRestartAction } = storeToRefs(unraidApiStore);
|
||||
|
||||
interface StatusOutput {
|
||||
icon: typeof BrandLoading | typeof ExclamationTriangleIcon | typeof CheckCircleIcon;
|
||||
iconClasses?: string;
|
||||
text: string;
|
||||
textClasses?: string;
|
||||
}
|
||||
const status = computed((): StatusOutput | undefined => {
|
||||
if (unraidApiStatus.value === 'connecting') {
|
||||
return {
|
||||
icon: BrandLoading,
|
||||
iconClasses: 'w-16px',
|
||||
text: props.t('Loading…'),
|
||||
textClasses: 'italic',
|
||||
};
|
||||
}
|
||||
if (unraidApiStatus.value === 'restarting') {
|
||||
return {
|
||||
icon: BrandLoading,
|
||||
iconClasses: 'w-16px',
|
||||
text: props.t('Restarting unraid-api…'),
|
||||
textClasses: 'italic',
|
||||
};
|
||||
}
|
||||
if (unraidApiStatus.value === 'offline') {
|
||||
return {
|
||||
icon: ExclamationTriangleIcon,
|
||||
iconClasses: 'text-red-500 w-16px h-16px',
|
||||
text: props.t('unraid-api is offline'),
|
||||
};
|
||||
}
|
||||
if (unraidApiStatus.value === 'online') {
|
||||
return {
|
||||
icon: CheckCircleIcon,
|
||||
iconClasses: 'text-green-600 w-16px h-16px',
|
||||
text: props.t('Connected'),
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li
|
||||
v-if="status"
|
||||
class="flex flex-row justify-start items-center gap-8px mt-8px px-8px"
|
||||
>
|
||||
<component
|
||||
:is="status.icon"
|
||||
:class="status.iconClasses"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span :class="status?.textClasses">
|
||||
{{ status.text }}
|
||||
</span>
|
||||
</li>
|
||||
<li v-if="unraidApiRestartAction" class="w-full">
|
||||
<UpcDropdownItem :item="unraidApiRestartAction" :t="t" />
|
||||
</li>
|
||||
</template>
|
||||
107
web/components/UserProfile/DropdownContent.vue
Normal file
107
web/components/UserProfile/DropdownContent.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { ArrowTopRightOnSquareIcon, CogIcon } from '@heroicons/vue/24/solid';
|
||||
|
||||
import { ACCOUNT, CONNECT_DASHBOARD, PLUGIN_SETTINGS } from '~/helpers/urls';
|
||||
import { useErrorsStore } from '~/store/errors';
|
||||
// import { usePromoStore } from '~/store/promo';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import type { UserProfileLink } from '~/types/userProfile';
|
||||
|
||||
const props = defineProps<{ t: any; }>();
|
||||
|
||||
const errorsStore = useErrorsStore();
|
||||
// const promoStore = usePromoStore();
|
||||
|
||||
const { keyActions, connectPluginInstalled, registered, stateData } = storeToRefs(useServerStore());
|
||||
const { errors } = storeToRefs(errorsStore);
|
||||
|
||||
const signInAction = computed(() => stateData.value.actions?.filter((act: { name: string; }) => act.name === 'signIn') ?? []);
|
||||
const signOutAction = computed(() => stateData.value.actions?.filter((act: { name: string; }) => act.name === 'signOut') ?? []);
|
||||
|
||||
const links = computed(():UserProfileLink[] => {
|
||||
return [
|
||||
...(registered.value && connectPluginInstalled.value
|
||||
? [
|
||||
{
|
||||
emphasize: true,
|
||||
external: true,
|
||||
href: CONNECT_DASHBOARD.toString(),
|
||||
icon: ArrowTopRightOnSquareIcon,
|
||||
text: props.t('Go to Connect'),
|
||||
title: props.t('Opens Connect in new tab'),
|
||||
},
|
||||
{
|
||||
external: true,
|
||||
href: ACCOUNT.toString(),
|
||||
icon: ArrowTopRightOnSquareIcon,
|
||||
text: props.t('Manage Unraid.net Account'),
|
||||
title: props.t('Manage Unraid.net Account in new tab'),
|
||||
},
|
||||
{
|
||||
href: PLUGIN_SETTINGS.toString(),
|
||||
icon: CogIcon,
|
||||
text: props.t('Settings'),
|
||||
title: props.t('Go to Connect plugin settings'),
|
||||
},
|
||||
...(signOutAction.value),
|
||||
]
|
||||
: []
|
||||
),
|
||||
...(!registered.value && connectPluginInstalled.value
|
||||
? [
|
||||
...(signInAction.value),
|
||||
]
|
||||
: []
|
||||
),
|
||||
// ...(!connectPluginInstalled.value
|
||||
// ? [
|
||||
// {
|
||||
// click: () => {
|
||||
// promoStore.promoShow();
|
||||
// },
|
||||
// icon: InformationCircleIcon,
|
||||
// text: props.t('Enhance your Unraid experience with Connect'),
|
||||
// title: props.t('Enhance your Unraid experience with Connect'),
|
||||
// },
|
||||
// ]
|
||||
// : []
|
||||
// ),
|
||||
];
|
||||
});
|
||||
|
||||
const showErrors = computed(() => errors.value.length);
|
||||
const showConnectStatus = computed(() => !showErrors.value && !stateData.value.error && registered.value && connectPluginInstalled.value);
|
||||
const showKeyline = computed(() => showConnectStatus.value && (keyActions.value?.length || links.value.length));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-y-8px min-w-300px max-w-350px">
|
||||
<header v-if="connectPluginInstalled" class="flex flex-row items-center justify-between mt-8px mx-8px">
|
||||
<h2 class="text-18px leading-none flex flex-row gap-x-8px items-center justify-between">
|
||||
<BrandLogoConnect gradient-start="currentcolor" gradient-stop="currentcolor" class="text-beta w-[120px]" />
|
||||
<UpcBeta />
|
||||
</h2>
|
||||
</header>
|
||||
<ul class="list-reset flex flex-col gap-y-4px p-0">
|
||||
<UpcDropdownConnectStatus v-if="showConnectStatus" :t="t" />
|
||||
<UpcDropdownError v-if="showErrors" :t="t" />
|
||||
|
||||
<li v-if="showKeyline" class="my-8px">
|
||||
<UpcKeyline />
|
||||
</li>
|
||||
|
||||
<template v-if="keyActions">
|
||||
<li v-for="action in keyActions" :key="action.name">
|
||||
<UpcDropdownItem :item="action" :t="t" />
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<template v-if="links.length">
|
||||
<li v-for="(link, index) in links" :key="`link_${index}`">
|
||||
<UpcDropdownItem :item="link" :t="t" />
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
27
web/components/UserProfile/DropdownError.vue
Normal file
27
web/components/UserProfile/DropdownError.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
// eslint-disable vue/no-v-html
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
import { useErrorsStore } from '~/store/errors';
|
||||
|
||||
defineProps<{ t: any; }>();
|
||||
|
||||
const errorsStore = useErrorsStore();
|
||||
const { errors } = storeToRefs(errorsStore);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul v-if="errors.length" class="list-reset flex flex-col gap-y-8px mb-4px border-2 border-solid border-unraid-red/90 rounded-md">
|
||||
<li v-for="(error, index) in errors" :key="index" class="flex flex-col gap-8px">
|
||||
<h3 class="text-18px py-4px px-12px text-white bg-unraid-red/90 font-semibold">
|
||||
<span>{{ t(error.heading) }}</span>
|
||||
</h3>
|
||||
<div class="text-14px px-12px flex flex-col gap-y-8px" :class="{ 'pb-8px': !error.actions }" v-html="t(error.message)" />
|
||||
<nav v-if="error.actions">
|
||||
<li v-for="(link, idx) in error.actions" :key="`link_${idx}`">
|
||||
<UpcDropdownItem :item="link" :rounded="false" :t="t" />
|
||||
</li>
|
||||
</nav>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
46
web/components/UserProfile/DropdownItem.vue
Normal file
46
web/components/UserProfile/DropdownItem.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid';
|
||||
import type { ServerStateDataAction } from '~/types/server';
|
||||
import type { UserProfileLink } from '~/types/userProfile';
|
||||
|
||||
export interface Props {
|
||||
item: ServerStateDataAction | UserProfileLink;
|
||||
rounded?: boolean;
|
||||
t: any;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
rounded: true,
|
||||
});
|
||||
|
||||
const showExternalIconOnHover = computed(() => props.item?.external && props.item.icon !== ArrowTopRightOnSquareIcon);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="item?.click ? 'button' : 'a'"
|
||||
:disabled="item?.disabled"
|
||||
:href="item?.href ?? null"
|
||||
:title="item?.title ? t(item?.title) : null"
|
||||
:target="item?.external ? '_blank' : null"
|
||||
:rel="item?.external ? 'noopener noreferrer' : null"
|
||||
class="text-left text-14px w-full flex flex-row items-center justify-between gap-x-8px px-8px py-8px cursor-pointer"
|
||||
:class="{
|
||||
'text-beta bg-transparent hover:text-white hover:bg-gradient-to-r hover:from-unraid-red hover:to-orange focus:text-white focus:bg-gradient-to-r focus:from-unraid-red focus:to-orange focus:outline-none': !item?.emphasize,
|
||||
'text-white bg-gradient-to-r from-unraid-red to-orange hover:from-unraid-red/60 hover:to-orange/60 focus:from-unraid-red/60 focus:to-orange/60': item?.emphasize,
|
||||
'group': showExternalIconOnHover,
|
||||
'rounded-md': rounded,
|
||||
'disabled:opacity-50 disabled:hover:opacity-50 disabled:focus:opacity-50 disabled:cursor-not-allowed': item?.disabled,
|
||||
}"
|
||||
@click.stop="item?.click ? item?.click() : null"
|
||||
>
|
||||
<span class="leading-snug inline-flex flex-row items-center gap-x-8px">
|
||||
<component :is="item?.icon" class="flex-shrink-0 text-current w-16px h-16px" aria-hidden="true" />
|
||||
{{ t(item?.text) }}
|
||||
</span>
|
||||
<ArrowTopRightOnSquareIcon
|
||||
v-if="showExternalIconOnHover"
|
||||
class="text-white fill-current flex-shrink-0 w-16px h-16px ml-8px opacity-0 group-hover:opacity-100 transition-opacity duration-200 ease-in-out"
|
||||
/>
|
||||
</component>
|
||||
</template>
|
||||
124
web/components/UserProfile/DropdownLaunchpad.vue
Normal file
124
web/components/UserProfile/DropdownLaunchpad.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<script lang="ts" setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import '~/assets/main.css';
|
||||
|
||||
const props = defineProps<{ t: any; }>();
|
||||
|
||||
const { expireTime, connectPluginInstalled, registered, state, stateData } = storeToRefs(useServerStore());
|
||||
|
||||
const showConnectCopy = computed(() => (connectPluginInstalled.value && !registered.value && !stateData.value?.error));
|
||||
|
||||
const heading = computed(() => {
|
||||
if (showConnectCopy.value) { return props.t('Thank you for installing Connect!'); }
|
||||
return props.t(stateData.value.heading);
|
||||
});
|
||||
|
||||
const subheading = computed(() => {
|
||||
if (showConnectCopy.value) { return props.t('Sign In to your Unraid.net account to get started'); }
|
||||
return props.t(stateData.value.message);
|
||||
});
|
||||
|
||||
const showExpireTime = computed(() => {
|
||||
return (state.value === 'TRIAL' || state.value === 'EEXPIRED') && expireTime.value > 0;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-y-24px w-full min-w-300px md:min-w-[500px] max-w-xl p-16px">
|
||||
<header :class="{ 'text-center': showConnectCopy }">
|
||||
<h2 class="text-24px text-center font-semibold" v-html="heading" />
|
||||
<div class="flex flex-col gap-y-8px" v-html="subheading" />
|
||||
<UpcUptimeExpire
|
||||
v-if="showExpireTime"
|
||||
class="text-center opacity-75 mt-12px"
|
||||
:t="t"
|
||||
/>
|
||||
</header>
|
||||
<ul v-if="stateData.actions" class="list-reset flex flex-col gap-y-8px px-16px">
|
||||
<li v-for="action in stateData.actions" :key="action.name">
|
||||
<BrandButton
|
||||
class="w-full"
|
||||
:disabled="action?.disabled"
|
||||
:external="action?.external"
|
||||
:href="action?.href"
|
||||
:icon="action.icon"
|
||||
:text="t(action.text)"
|
||||
@click="action.click()"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="postcss">
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.DropdownWrapper_blip {
|
||||
box-shadow: var(--ring-offset-shadow), var(--ring-shadow), var(--shadow-beta);
|
||||
|
||||
&::before {
|
||||
@apply absolute z-20 block;
|
||||
|
||||
content: '';
|
||||
width: 0;
|
||||
height: 0;
|
||||
top: -10px;
|
||||
right: 42px;
|
||||
border-right: 11px solid transparent;
|
||||
border-bottom: 11px solid var(--color-alpha);
|
||||
border-left: 11px solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.unraid_mark_2,
|
||||
.unraid_mark_4 {
|
||||
animation: mark_2 1.5s ease infinite;
|
||||
}
|
||||
.unraid_mark_3 {
|
||||
animation: mark_3 1.5s ease infinite;
|
||||
}
|
||||
.unraid_mark_6,
|
||||
.unraid_mark_8 {
|
||||
animation: mark_6 1.5s ease infinite;
|
||||
}
|
||||
.unraid_mark_7 {
|
||||
animation: mark_7 1.5s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes mark_2 {
|
||||
50% {
|
||||
transform: translateY(-40px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes mark_3 {
|
||||
50% {
|
||||
transform: translateY(-62px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes mark_6 {
|
||||
50% {
|
||||
transform: translateY(40px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes mark_7 {
|
||||
50% {
|
||||
transform: translateY(62px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
56
web/components/UserProfile/DropdownTrigger.vue
Normal file
56
web/components/UserProfile/DropdownTrigger.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { InformationCircleIcon, ExclamationTriangleIcon, ShieldExclamationIcon } from '@heroicons/vue/24/solid';
|
||||
|
||||
import { useDropdownStore } from '~/store/dropdown';
|
||||
import { useErrorsStore } from '~/store/errors';
|
||||
import { useServerStore } from '~/store/server';
|
||||
|
||||
const props = defineProps<{ t: any; }>();
|
||||
|
||||
const dropdownStore = useDropdownStore();
|
||||
const { dropdownVisible } = storeToRefs(dropdownStore);
|
||||
const { errors } = storeToRefs(useErrorsStore());
|
||||
const {
|
||||
connectPluginInstalled,
|
||||
registered,
|
||||
state,
|
||||
stateData,
|
||||
username,
|
||||
} = storeToRefs(useServerStore());
|
||||
|
||||
const registeredAndconnectPluginInstalled = computed(() => connectPluginInstalled.value && registered.value);
|
||||
const showErrorIcon = computed(() => errors.value.length || stateData.value.error);
|
||||
|
||||
const text = computed((): string | undefined => {
|
||||
if ((stateData.value.error) && state.value !== 'EEXPIRED') { return props.t('Fix Error'); }
|
||||
if (registeredAndconnectPluginInstalled.value) { return username.value; }
|
||||
});
|
||||
|
||||
const title = computed((): string => {
|
||||
if (state.value === 'ENOKEYFILE') { return props.t('Get Started'); }
|
||||
if (state.value === 'EEXPIRED') { return props.t('Trial Expired, see options below'); }
|
||||
if (showErrorIcon.value) { return props.t('Learn more about the error'); }
|
||||
return dropdownVisible.value ? props.t('Close Dropdown') : props.t('Open Dropdown');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="group text-18px hover:text-alpha focus:text-alpha border-0 relative flex flex-row justify-end items-center h-full gap-x-8px outline-none focus:outline-none"
|
||||
:title="title"
|
||||
@click="dropdownStore.dropdownToggle()"
|
||||
>
|
||||
<template v-if="errors.length && errors[0].level">
|
||||
<InformationCircleIcon v-if="errors[0].level === 'info'" class="text-unraid-red fill-current relative w-24px h-24px" />
|
||||
<ExclamationTriangleIcon v-if="errors[0].level === 'warning'" class="text-unraid-red fill-current relative w-24px h-24px" />
|
||||
<ShieldExclamationIcon v-if="errors[0].level === 'error'" class="text-unraid-red fill-current relative w-24px h-24px" />
|
||||
</template>
|
||||
<span v-if="text" class="relative leading-none">
|
||||
<span>{{ text }}</span>
|
||||
<span class="absolute bottom-[-3px] inset-x-0 h-2px w-full bg-gradient-to-r from-unraid-red to-orange rounded opacity-0 group-hover:opacity-100 group-focus:opacity-100 transition-opacity" />
|
||||
</span>
|
||||
<UpcDropdownTriggerMenuIcon :open="dropdownVisible" />
|
||||
<BrandAvatar />
|
||||
</button>
|
||||
</template>
|
||||
14
web/components/UserProfile/DropdownTriggerMenuIcon.vue
Normal file
14
web/components/UserProfile/DropdownTriggerMenuIcon.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { Bars3Icon, XMarkIcon } from '@heroicons/vue/24/solid';
|
||||
|
||||
export interface Props {
|
||||
open?: boolean;
|
||||
}
|
||||
withDefaults(defineProps<Props>(), {
|
||||
open: false,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Bars3Icon v-if="!open" class="w-20px" />
|
||||
<XMarkIcon v-else class="w-20px" />
|
||||
</template>
|
||||
10
web/components/UserProfile/DropdownWrapper.vue
Normal file
10
web/components/UserProfile/DropdownWrapper.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
// shadow-[var(--ring-offset-shadow)_var(--ring-shadow)_var(--shadow-beta)]
|
||||
// border border-solid border-beta/5
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="flex flex-col gap-y-8px p-8px bg-alpha rounded-lg shadow-xl shadow-orange/10">
|
||||
<slot />
|
||||
</nav>
|
||||
</template>
|
||||
3
web/components/UserProfile/Keyline.vue
Normal file
3
web/components/UserProfile/Keyline.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<hr class="w-full h-2px bg-gradient-to-r from-unraid-red to-orange shadow-none border-none rounded">
|
||||
</template>
|
||||
141
web/components/UserProfile/Promo.vue
Normal file
141
web/components/UserProfile/Promo.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* @todo devEnv should be a .env variable so we can gate staging installs
|
||||
*
|
||||
* @todo future idea – turn this into a carousel. each feature could have a short video if we ever them
|
||||
*/
|
||||
import { Switch, SwitchGroup, SwitchLabel } from '@headlessui/vue';
|
||||
import { ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid';
|
||||
|
||||
import useInstallPlugin from '~/composables/installPlugin';
|
||||
import { CONNECT_DOCS } from '~/helpers/urls';
|
||||
import { usePromoStore } from '~/store/promo';
|
||||
import type { UserProfilePromoFeature } from '~/types/userProfile';
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import '~/assets/main.css';
|
||||
|
||||
export interface Props {
|
||||
open?: boolean;
|
||||
t: any;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
open: false,
|
||||
});
|
||||
|
||||
const promoStore = usePromoStore();
|
||||
|
||||
/**
|
||||
* These are translated in the component below. So if you add a new feature, make sure to add it to the translation file.
|
||||
*/
|
||||
const features = ref<UserProfilePromoFeature[]>([
|
||||
{
|
||||
title: 'Dynamic Remote Access',
|
||||
copy: 'Toggle on/off server accessibility with dynamic remote access. Automatically turn on UPnP and open a random WAN port on your router at the click of a button and close off access in seconds.',
|
||||
},
|
||||
{
|
||||
title: 'Manage Your Server Within Connect',
|
||||
copy: 'Servers equipped with a myunraid.net certificate can be managed directly from within the Connect web UI. Manage multiple servers from your phone, tablet, laptop, or PC in the same browser window.',
|
||||
},
|
||||
{
|
||||
title: 'Deep Linking',
|
||||
copy: 'The Connect dashboard links to relevant sections of the webgui, allowing quick access to those settings and server sections.',
|
||||
},
|
||||
{
|
||||
title: 'Online Flash Backup',
|
||||
copy: 'Never ever be left without a backup of your config. If you need to change flash drives, generate a backup from Connect and be up and running in minutes.',
|
||||
},
|
||||
{
|
||||
title: 'Real-time Monitoring',
|
||||
copy: 'Get an overview of your server\'s state, storage space, apps and VMs status, and more.',
|
||||
},
|
||||
{
|
||||
title: 'Customizable Dashboard Tiles',
|
||||
copy: 'Set custom server tiles how you like and automatically display your server\'s banner image on your Connect Dashboard.',
|
||||
},
|
||||
{
|
||||
title: 'License Management',
|
||||
copy: 'Manage your license keys at any time via the My Keys section.',
|
||||
},
|
||||
{
|
||||
title: 'Plus more on the way',
|
||||
copy: 'All you need is an active internet connection, an Unraid.net account, and the Connect plugin. Get started by installing the plugin.',
|
||||
},
|
||||
]);
|
||||
|
||||
const staging = ref(false);
|
||||
const { install } = useInstallPlugin();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
:t="t"
|
||||
:title="t('Introducing Unraid Connect')"
|
||||
:description="t('Enhance your Unraid experience')"
|
||||
:open="open"
|
||||
:show-close-x="true"
|
||||
max-width="max-w-800px"
|
||||
@close="promoStore.promoHide()"
|
||||
>
|
||||
<template #headerTitle>
|
||||
<span><UpcBeta class="relative -top-1" /></span>
|
||||
</template>
|
||||
|
||||
<template #main>
|
||||
<div class="text-center relative w-full">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 justify-center p-16px md:py-24px gap-16px">
|
||||
<UpcPromoFeature
|
||||
v-for="(feature, index) in features"
|
||||
:key="index"
|
||||
:title="t(feature.title)"
|
||||
:copy="t(feature.copy)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="w-full max-w-xs flex flex-col items-center gap-y-16px mx-auto">
|
||||
<SwitchGroup v-if="import.meta.env.DEV" as="div" class="flex items-center justify-center">
|
||||
<Switch v-model="staging" :class="[staging ? 'bg-indigo-600' : 'bg-gray-200', 'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2']">
|
||||
<span aria-hidden="true" :class="[staging ? 't-x-5' : 't-x-0', 'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out']" />
|
||||
</Switch>
|
||||
<SwitchLabel as="span" class="ml-3 text-12px">
|
||||
<span class="font-semibold">Install Staging</span>
|
||||
</SwitchLabel>
|
||||
</SwitchGroup>
|
||||
<button
|
||||
class="text-white text-14px text-center w-full flex flex-row items-center justify-center gap-x-8px px-8px py-8px cursor-pointer rounded-md bg-gradient-to-r from-unraid-red to-orange hover:from-unraid-red/60 hover:to-orange/60 focus:from-unraid-red/60 focus:to-orange/60"
|
||||
@click="install({ staging, update: false })"
|
||||
>
|
||||
{{ staging ? 'Install Connect Staging' : t('Install Connect') }}
|
||||
</button>
|
||||
<div>
|
||||
<a
|
||||
:href="CONNECT_DOCS.toString()"
|
||||
class="text-12px tracking-wide inline-flex flex-row items-center justify-start gap-8px mx-8px opacity-60 hover:opacity-100 focus:opacity-100 underline transition"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
:title="t('Checkout the Connect Documentation')"
|
||||
>
|
||||
{{ t('Learn More') }}
|
||||
<ArrowTopRightOnSquareIcon class="w-16px" />
|
||||
</a>
|
||||
<button
|
||||
class="text-12px tracking-wide inline-block mx-8px opacity-60 hover:opacity-100 focus:opacity-100 underline transition"
|
||||
:title="t('Close')"
|
||||
@click="promoStore.promoHide()"
|
||||
>
|
||||
{{ t('No thanks') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style lang="postcss">
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
</style>
|
||||
39
web/components/UserProfile/PromoFeature.vue
Normal file
39
web/components/UserProfile/PromoFeature.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
export interface Props {
|
||||
center?: boolean;
|
||||
copy?: string;
|
||||
icon?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="text-left relative flex overflow-hidden">
|
||||
<span v-if="!center" class="flex-shrink-0">
|
||||
<slot />
|
||||
</span>
|
||||
<div class="inline-flex flex-col" :class="{ 'text-center': center }">
|
||||
<h3
|
||||
class="text-16px font-semibold"
|
||||
:class="{
|
||||
'mt-0 mb-4px': copy,
|
||||
'my-0': !copy,
|
||||
'flex flex-row justify-center items-center': center
|
||||
}"
|
||||
>
|
||||
<span v-if="center" class="flex-shrink-0 mr-8px">
|
||||
<slot />
|
||||
</span>
|
||||
{{ title }}
|
||||
</h3>
|
||||
<p
|
||||
v-if="copy"
|
||||
class="text-14px opacity-75 py-0"
|
||||
:class="{'px-8px': center}"
|
||||
v-html="copy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
42
web/components/UserProfile/ServerState.vue
Normal file
42
web/components/UserProfile/ServerState.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
import { useServerStore } from '~/store/server';
|
||||
import type { ServerStateDataAction } from '~/types/server';
|
||||
|
||||
defineProps<{ t: any; }>();
|
||||
|
||||
const { state, stateData } = storeToRefs(useServerStore());
|
||||
|
||||
const purchaseAction = computed((): ServerStateDataAction | undefined => {
|
||||
return stateData.value.actions && stateData.value.actions.find(action => action.name === 'purchase');
|
||||
});
|
||||
const upgradeAction = computed((): ServerStateDataAction | undefined => {
|
||||
return stateData.value.actions && stateData.value.actions.find(action => action.name === 'upgrade');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="flex flex-row items-center gap-x-8px">
|
||||
<template v-if="upgradeAction">
|
||||
<UpcServerStateBuy
|
||||
class="text-gamma"
|
||||
:title="t('Upgrade Key')"
|
||||
@click="upgradeAction.click()"
|
||||
>
|
||||
<h5>Unraid OS <em><strong>{{ t(stateData.humanReadable) }}</strong></em></h5>
|
||||
</UpcServerStateBuy>
|
||||
</template>
|
||||
<h5 v-else>
|
||||
Unraid OS <em :class="{ 'text-unraid-red': stateData.error || state === 'EEXPIRED' }"><strong>{{ t(stateData.humanReadable) }}</strong></em>
|
||||
</h5>
|
||||
|
||||
<template v-if="purchaseAction">
|
||||
<UpcServerStateBuy
|
||||
class="text-orange-dark relative top-[1px] hidden sm:block"
|
||||
:title="t('Purchase Key')"
|
||||
@click="purchaseAction.click()"
|
||||
>{{ t('Purchase') }}</UpcServerStateBuy>
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
5
web/components/UserProfile/ServerStateBuy.vue
Normal file
5
web/components/UserProfile/ServerStateBuy.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<button class="text-12px font-semibold transition-colors duration-150 ease-in-out border-t-0 border-l-0 border-r-0 border-b-2 border-transparent hover:border-orange-dark focus:border-orange-dark focus:outline-none">
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
82
web/components/UserProfile/Trial.vue
Normal file
82
web/components/UserProfile/Trial.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<script lang="ts" setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useTrialStore } from '~/store/trial';
|
||||
|
||||
export interface Props {
|
||||
open?: boolean;
|
||||
t: any;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
open: false,
|
||||
});
|
||||
|
||||
const trialStore = useTrialStore();
|
||||
const { trialModalLoading, trialStatus } = storeToRefs(trialStore);
|
||||
|
||||
interface TrialStatusCopy {
|
||||
heading: string;
|
||||
subheading?: string;
|
||||
}
|
||||
const trialStatusCopy = computed((): TrialStatusCopy | null => {
|
||||
switch (trialStatus.value) {
|
||||
case 'failed':
|
||||
return {
|
||||
heading: props.t('Trial Key Creation Failed'),
|
||||
subheading: props.t('Error creatiing a trial key. Please try again later.'),
|
||||
};
|
||||
case 'trialExtend':
|
||||
return {
|
||||
heading: props.t('Extending your free trial by 15 days'),
|
||||
subheading: props.t('Please keep this window open'),
|
||||
};
|
||||
case 'trialStart':
|
||||
return {
|
||||
heading: props.t('Starting your free 30 day trial'),
|
||||
subheading: props.t('Please keep this window open'),
|
||||
};
|
||||
case 'success':
|
||||
return {
|
||||
heading: props.t('Trial Key Created'),
|
||||
subheading: props.t('Please wait while the page reloads to install your trial key'),
|
||||
};
|
||||
case 'ready':
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const close = () => {
|
||||
if (trialStatus.value === 'trialStart') { return console.debug('[close] not allowed'); }
|
||||
trialStore.setTrialStatus('ready');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
:t="t"
|
||||
:open="open"
|
||||
:title="trialStatusCopy?.heading"
|
||||
:description="trialStatusCopy?.subheading"
|
||||
:show-close-x="!trialModalLoading"
|
||||
max-width="max-w-640px"
|
||||
@close="close"
|
||||
>
|
||||
<template #main>
|
||||
<BrandLoading v-if="trialModalLoading" class="w-[150px] mx-auto my-24px" />
|
||||
</template>
|
||||
|
||||
<template v-if="!trialModalLoading" #footer>
|
||||
<div class="w-full max-w-xs flex flex-col items-center gap-y-16px mx-auto">
|
||||
<div>
|
||||
<button
|
||||
class="text-12px tracking-wide inline-block mx-8px opacity-60 hover:opacity-100 focus:opacity-100 underline transition"
|
||||
:title="t('Close Modal')"
|
||||
@click="close"
|
||||
>
|
||||
{{ t('Close') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
78
web/components/UserProfile/UptimeExpire.vue
Normal file
78
web/components/UserProfile/UptimeExpire.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
import useTimeHelper from '~/composables/time';
|
||||
import { useServerStore } from '~/store/server';
|
||||
|
||||
export interface Props {
|
||||
forExpire?: boolean;
|
||||
t: any;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
forExpire: false,
|
||||
});
|
||||
|
||||
const { buildStringFromValues, dateDiff, formatDate } = useTimeHelper(props.t);
|
||||
|
||||
const serverStore = useServerStore();
|
||||
const { uptime, expireTime, state } = storeToRefs(serverStore);
|
||||
|
||||
const time = computed(() => {
|
||||
if (props.forExpire && expireTime.value) {
|
||||
return expireTime.value;
|
||||
}
|
||||
return (state.value === 'TRIAL' || state.value === 'EEXPIRED') && expireTime.value && expireTime.value > 0
|
||||
? expireTime.value
|
||||
: uptime.value;
|
||||
});
|
||||
|
||||
const parsedTime = ref<string>('');
|
||||
const formattedTime = computed<string>(() => formatDate(time.value));
|
||||
|
||||
const countUp = computed<boolean>(() => {
|
||||
if (props.forExpire && expireTime.value) {
|
||||
return false;
|
||||
}
|
||||
return state.value !== 'TRIAL' && state.value !== 'ENOCONN';
|
||||
});
|
||||
|
||||
const output = computed(() => {
|
||||
if (!countUp.value || state.value === 'EEXPIRED') {
|
||||
return {
|
||||
title: state.value === 'EEXPIRED'
|
||||
? props.t('Trial Key Expired at {0}', [formattedTime.value])
|
||||
: props.t('Trial Key Expires at {0}', [formattedTime.value]),
|
||||
text: state.value === 'EEXPIRED'
|
||||
? props.t('Trial Key Expired {0}', [parsedTime.value])
|
||||
: props.t('Trial Key Expires in {0}', [parsedTime.value]),
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: props.t('Server Up Since {0}', [formattedTime.value]),
|
||||
text: props.t('Uptime {0}', [parsedTime.value]),
|
||||
};
|
||||
});
|
||||
|
||||
const runDiff = () => {
|
||||
parsedTime.value = buildStringFromValues(dateDiff((time.value).toString(), countUp.value));
|
||||
};
|
||||
|
||||
let interval: string | number | NodeJS.Timeout | undefined;
|
||||
onBeforeMount(() => {
|
||||
runDiff();
|
||||
interval = setInterval(() => {
|
||||
runDiff();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(interval);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p :title="output.title">
|
||||
{{ output.text }}
|
||||
</p>
|
||||
</template>
|
||||
Reference in New Issue
Block a user