mirror of
https://github.com/unraid/api.git
synced 2026-05-06 15:11:44 -05:00
feat(web): registration component ui / ux
This commit is contained in:
@@ -30,7 +30,31 @@ const randomGuid = `1111-1111-${makeid(4)}-123412341234`; // this guid is regist
|
||||
// EBLACKLISTED2
|
||||
// ENOCONN
|
||||
const state: ServerState = 'TRIAL';
|
||||
const regTy = 'Trial';
|
||||
let regTy = '';
|
||||
switch (state) {
|
||||
// @ts-ignore
|
||||
case 'EEXPIRED':
|
||||
// @ts-ignore
|
||||
case 'ENOCONN':
|
||||
// @ts-ignore
|
||||
case 'TRIAL':
|
||||
regTy = 'Trial';
|
||||
break;
|
||||
// @ts-ignore
|
||||
case 'BASIC':
|
||||
// @ts-ignore
|
||||
case 'PLUS':
|
||||
// @ts-ignore
|
||||
case 'PRO':
|
||||
// @ts-ignore
|
||||
case 'STARTER':
|
||||
// @ts-ignore
|
||||
case 'UNLEASHED':
|
||||
// @ts-ignore
|
||||
case 'LIFETIME':
|
||||
regTy = state.charAt(0).toUpperCase() + state.substring(1).toLowerCase(); // title case
|
||||
break;
|
||||
}
|
||||
|
||||
const uptime = Date.now() - 60 * 60 * 1000; // 1 hour ago
|
||||
const oneHourFromNow = Date.now() + 60 * 60 * 1000; // 1 hour from now
|
||||
@@ -77,7 +101,7 @@ export const serverState: Server = {
|
||||
bgColor: '',
|
||||
descriptionShow: true,
|
||||
metaColor: '',
|
||||
name: 'white',
|
||||
name: 'black',
|
||||
textColor: ''
|
||||
},
|
||||
uptime,
|
||||
|
||||
@@ -15,6 +15,8 @@ else
|
||||
echo "Third party plugins found - PLEASE CHECK YOUR UNRAID NOTIFICATIONS AND WAIT FOR THE MESSAGE THAT IT IS SAFE TO REBOOT!"
|
||||
fi
|
||||
*/
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import '~/assets/main.css';
|
||||
import {
|
||||
ShieldCheckIcon,
|
||||
ShieldExclamationIcon,
|
||||
@@ -24,12 +26,12 @@ import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { useServerStore } from '~/store/server';
|
||||
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import '~/assets/main.css';
|
||||
import { RegistrationItemProps } from '~/types/registration';
|
||||
|
||||
import KeyActions from '~/components/KeyActions.vue';
|
||||
import RegistrationReplaceCheck from '~/components/Registration/ReplaceCheck.vue';
|
||||
import RegistrationUpgradeExpiration from '~/components/Registration/UpgradeExpiration.vue';
|
||||
import UserProfileUptimeExpire from '~/components/UserProfile/UptimeExpire.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -61,41 +63,83 @@ const {
|
||||
stateDataError,
|
||||
} = storeToRefs(serverStore);
|
||||
|
||||
const devicesAvailable = computed(() => {
|
||||
const devicesAvailable = computed((): number => {
|
||||
switch(regTy.value) {
|
||||
case 'Starter':
|
||||
return 4;
|
||||
case 'Basic':
|
||||
return 6;
|
||||
case 'Plus':
|
||||
return 12;
|
||||
case 'Unleashed':
|
||||
case 'Lifetime':
|
||||
case 'Pro':
|
||||
case 'Trial':
|
||||
return 'unlimited';
|
||||
return 9999;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
const items = computed(() => {
|
||||
const items = computed((): RegistrationItemProps[] => {
|
||||
return [
|
||||
...(regTy.value ? [{ label: t('License key type'), text: regTy.value }] : []),
|
||||
...(regTo.value ? [ { label: t('Registered to'), text: regTo.value }] : []),
|
||||
...(regTo.value ? [{ label: t('Registered on'), text: dayjs(regTm.value).format('YYYY-MM-DD HH:mm')}] : []),
|
||||
/**
|
||||
* @todo factor in grandfathered users and display a different message
|
||||
*/
|
||||
...(regUpdExpAt.value
|
||||
? [{
|
||||
...(regTy.value ? [{
|
||||
label: t('License key type'),
|
||||
text: regTy.value,
|
||||
}] : []),
|
||||
...(state.value === 'TRIAL' || state.value === 'EEXPIRED' ? [{
|
||||
error: state.value === 'EEXPIRED',
|
||||
label: t('Trial expiration'),
|
||||
component: UserProfileUptimeExpire,
|
||||
componentProps: {
|
||||
forExpire: true,
|
||||
shortText: true,
|
||||
t: t,
|
||||
},
|
||||
componentOpacity: true,
|
||||
}] : []),
|
||||
...(regTo.value ? [{
|
||||
label: t('Registered to'),
|
||||
text: regTo.value,
|
||||
}] : []),
|
||||
...(regTo.value && regTm.value ? [{
|
||||
label: t('Registered on'),
|
||||
text: dayjs(regTm.value).format('YYYY-MM-DD HH:mm'),
|
||||
}] : []),
|
||||
...(regUpdExpAt.value && (state.value === 'STARTER' || state.value === 'UNLEASHED') ? [{
|
||||
error: regUpdExpired.value,
|
||||
label: t('OS Update Eligibility'),
|
||||
component: RegistrationUpgradeExpiration,
|
||||
componentProps: { t: t },
|
||||
}]
|
||||
: []),
|
||||
...(state.value === 'EGUID' ? [{ label: t('Registered GUID'), text: regGuid.value }] : [] ),
|
||||
{ label: t('Flash GUID'), text: guid.value },
|
||||
{ label: t('Flash Vendor'), text: flashVendor.value },
|
||||
{ label: t('Flash Product'), text: flashProduct.value },
|
||||
{ label: t('Attached Storage Devices'), text: t('{0} out of {1} devices', [deviceCount.value, devicesAvailable.value]) },
|
||||
...(regUpdExpAt.value
|
||||
? [{
|
||||
...(state.value === 'EGUID' ? [{
|
||||
label: t('Registered GUID'),
|
||||
text: regGuid.value,
|
||||
}] : [] ),
|
||||
...(guid.value ? [{
|
||||
label: t('Flash GUID'),
|
||||
text: guid.value,
|
||||
}] : [] ),
|
||||
...(flashVendor.value ? [{
|
||||
label: t('Flash Vendor'),
|
||||
text: flashVendor.value,
|
||||
}] : [] ),
|
||||
...(flashProduct.value ? [{
|
||||
label: t('Flash Product'),
|
||||
text: flashProduct.value,
|
||||
}] : [] ),
|
||||
...(!stateDataError.value ? [{
|
||||
error: deviceCount.value > devicesAvailable.value,
|
||||
label: t('Attached Storage Devices'),
|
||||
text: t('{0} out of {1} devices', [deviceCount.value, devicesAvailable.value > 12 ? t('unlimited') : devicesAvailable.value]),
|
||||
}] : []),
|
||||
...(!stateDataError.value && guid.value ? [{
|
||||
label: t('Key Replacement Eligibility'),
|
||||
component: RegistrationReplaceCheck,
|
||||
componentProps: { t: t },
|
||||
}] : []),
|
||||
...(keyActions.value ? [{
|
||||
label: t('License key actions'),
|
||||
component: KeyActions,
|
||||
componentProps: { t: t },
|
||||
@@ -135,7 +179,10 @@ const items = computed(() => {
|
||||
:text="item.text"
|
||||
>
|
||||
<template v-if="item.component" #right>
|
||||
<component :is="item.component" v-bind="item.componentProps" />
|
||||
<component
|
||||
:is="item.component"
|
||||
v-bind="item.componentProps"
|
||||
:class="[item.componentOpacity && !item.error ? 'opacity-75' : '']" />
|
||||
</template>
|
||||
</RegistrationItem>
|
||||
</dl>
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { ShieldExclamationIcon } from '@heroicons/vue/24/solid';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeStore } from '~/store/theme';
|
||||
|
||||
export interface Props {
|
||||
error?: boolean;
|
||||
label?: string;
|
||||
text?: number | string | undefined;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
import { useThemeStore } from '~/store/theme';
|
||||
import { RegistrationItemProps } from '~/types/registration';
|
||||
|
||||
withDefaults(defineProps<RegistrationItemProps>(), {
|
||||
error: false,
|
||||
label: '',
|
||||
text: '',
|
||||
});
|
||||
|
||||
const { darkMode } = storeToRefs(useThemeStore());
|
||||
@@ -19,10 +14,6 @@ const { darkMode } = storeToRefs(useThemeStore());
|
||||
const evenBgColor = computed(() => {
|
||||
return darkMode.value ? 'even:bg-grey-darkest' : 'even:bg-black/5';
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
console.debug('[Item.onMounted]', props);
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -31,13 +22,13 @@ onMounted(() => {
|
||||
!error && evenBgColor,
|
||||
error && 'text-white bg-unraid-red',
|
||||
]"
|
||||
class="text-16px p-16px sm:px-20px sm:grid sm:grid-cols-3 sm:gap-16px items-start"
|
||||
class="text-16px p-16px grid grid-cols-1 gap-4px sm:px-20px sm:grid-cols-3 sm:gap-16px items-start"
|
||||
>
|
||||
<dt class="font-semibold flex flex-row justify-start items-center gap-x-8px">
|
||||
<ShieldExclamationIcon v-if="error" class="w-16px h-16px fill-current" />
|
||||
<span>{{ label }}</span>
|
||||
</dt>
|
||||
<dd class="mt-4px leading-normal sm:col-span-2 sm:mt-0">
|
||||
<dd class="leading-normal sm:col-span-2">
|
||||
<span v-if="text" class="opacity-75 select-all">{{ text }}</span>
|
||||
<template v-if="$slots['right']">
|
||||
<slot name="right"></slot>
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
KeyIcon,
|
||||
XCircleIcon,
|
||||
ShieldExclamationIcon,
|
||||
} from '@heroicons/vue/24/solid';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import type { WretchError } from 'wretch';
|
||||
|
||||
import { validateGuid, type ValidateGuidPayload } from '~/composables/services/keyServer';
|
||||
import { useServerStore } from '~/store/server';
|
||||
|
||||
import BrandLoadingWhite from '~/components/Brand/LoadingWhite.vue';
|
||||
import { storeKeyNameFromField } from '@apollo/client/utilities';
|
||||
|
||||
const props = defineProps<{
|
||||
t: any;
|
||||
}>();
|
||||
|
||||
const { guid } = storeToRefs(useServerStore());
|
||||
|
||||
const error = ref<{
|
||||
name: string;
|
||||
message: string;
|
||||
stack?: string | undefined;
|
||||
cause?: unknown;
|
||||
} | null>(null);
|
||||
const status = ref<'checking' | 'eligible' | 'error' | 'ineligible' | 'ready'>(guid.value ? 'ready' : 'error');
|
||||
const statusOutput = computed(() => {
|
||||
switch (status.value) {
|
||||
case 'eligible':
|
||||
return {
|
||||
classes: 'text-green-500',
|
||||
icon: CheckCircleIcon,
|
||||
text: props.t('Eligible'),
|
||||
};
|
||||
|
||||
case 'ineligible':
|
||||
return {
|
||||
classes: 'text-red-500',
|
||||
icon: XCircleIcon,
|
||||
text: props.t('Ineligible'),
|
||||
};
|
||||
|
||||
case 'error':
|
||||
return {
|
||||
classes: 'text-red-500',
|
||||
icon: ShieldExclamationIcon,
|
||||
text: error.value?.message || 'Unknown error',
|
||||
};
|
||||
|
||||
default: return null;
|
||||
}
|
||||
});
|
||||
const validationResponse = ref<ValidateGuidPayload | undefined>(sessionStorage.getItem('replaceCheck') ? JSON.parse(sessionStorage.getItem('replaceCheck') as string) : undefined);
|
||||
|
||||
const check = async () => {
|
||||
if (!guid.value) {
|
||||
status.value = 'error';
|
||||
error.value = { name: 'Error', message: props.t('Flash GUID required') };
|
||||
}
|
||||
|
||||
try {
|
||||
status.value = 'checking';
|
||||
error.value = null;
|
||||
const response: ValidateGuidPayload = await validateGuid({ guid: guid.value }).json();
|
||||
sessionStorage.setItem('replaceCheck', JSON.stringify(response));
|
||||
status.value = response?.replaceable ? 'eligible' : 'ineligible';
|
||||
} catch (err) {
|
||||
const catchError = err as WretchError;
|
||||
status.value = 'error';
|
||||
error.value = catchError?.message ? catchError : { name: 'Error', message: 'Unknown error' };
|
||||
console.error('[ReplaceCheck.check]', catchError);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* If we already have a validation response, set the status to eligible or ineligible
|
||||
*/
|
||||
onBeforeMount(() => {
|
||||
if (validationResponse.value) {
|
||||
status.value = validationResponse.value?.replaceable ? 'eligible' : 'ineligible';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<BrandButton
|
||||
v-if="status === 'checking' || status === 'ready'"
|
||||
@click="check"
|
||||
:disabled="status !== 'ready'"
|
||||
:icon="status === 'checking' ? BrandLoadingWhite : KeyIcon"
|
||||
:text="t('Check Eligibility')"
|
||||
class="w-full sm:max-w-300px"
|
||||
/>
|
||||
|
||||
<p
|
||||
v-else-if="statusOutput"
|
||||
class="flex flex-row items-center gap-x-4px"
|
||||
:class="[statusOutput?.classes]"
|
||||
>
|
||||
<component
|
||||
v-if="statusOutput?.icon"
|
||||
:is="statusOutput?.icon"
|
||||
class="w-16px fill-current" />
|
||||
{{ statusOutput?.text }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -6,11 +6,13 @@ import { useServerStore } from '~/store/server';
|
||||
|
||||
export interface Props {
|
||||
forExpire?: boolean;
|
||||
shortText?: boolean;
|
||||
t: any;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
forExpire: false,
|
||||
shortText: false,
|
||||
});
|
||||
|
||||
const { buildStringFromValues, dateDiff, formatDate } = useTimeHelper(props.t);
|
||||
@@ -41,11 +43,11 @@ 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]),
|
||||
? props.t(props.shortText ? 'Expired at {0}' : 'Trial Key Expired at {0}', [formattedTime.value])
|
||||
: props.t(props.shortText ? 'Expires at {0}' : '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]),
|
||||
? props.t(props.shortText ? 'Expired {0}' : 'Trial Key Expired {0}', [parsedTime.value])
|
||||
: props.t(props.shortText ? 'Expires in {0}' : 'Trial Key Expires in {0}', [parsedTime.value]),
|
||||
};
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -15,6 +15,13 @@ export const startTrial = (payload: StartTrialPayload) => KeyServer
|
||||
.formUrl(payload)
|
||||
.post();
|
||||
|
||||
export interface ValidateGuidPayload {
|
||||
purchaseable: true,
|
||||
registered: false,
|
||||
replaceable: false,
|
||||
upgradeable: false,
|
||||
upgradeAllowed: 'pro' | 'plus' | 'unleashed'[],
|
||||
}
|
||||
export const validateGuid = (payload: { guid: string }) => KeyServer
|
||||
.url('/validate/guid')
|
||||
.formUrl(payload)
|
||||
|
||||
@@ -251,6 +251,8 @@
|
||||
"Flash Product": "Flash Product",
|
||||
"Attached Storage Devices": "Attached Storage Devices",
|
||||
"{0} out of {1} devices": "{0} out of {1} devices",
|
||||
"{0} devices": "{0} devices",
|
||||
"unlimited": "unlimited",
|
||||
"Unable to check for updates": "Unable to check for updates",
|
||||
"License key actions": "License key actions",
|
||||
"License key type": "License key type",
|
||||
@@ -260,5 +262,9 @@
|
||||
"Expired {0}": "Expired {0}",
|
||||
"Expires in {0}": "Expires in {0}",
|
||||
"Renew your license key now": "Renew your license key now",
|
||||
"Renew Key to Enable OS Updates": "Renew Key to Enable OS Updates"
|
||||
"Renew Key to Enable OS Updates": "Renew Key to Enable OS Updates",
|
||||
"Check Eligibility": "Check Eligibility",
|
||||
"Eligible": "Eligible",
|
||||
"Ineligible": "Ineligible",
|
||||
"Flash GUID required": "Flash GUID required"
|
||||
}
|
||||
|
||||
@@ -50,10 +50,23 @@ export const usePurchaseStore = defineStore('purchase', () => {
|
||||
serverStore.inIframe,
|
||||
);
|
||||
};
|
||||
const renew = () => {
|
||||
callbackStore.send(
|
||||
PURCHASE_CALLBACK.toString(),
|
||||
[{
|
||||
server: {
|
||||
...serverStore.serverPurchasePayload,
|
||||
},
|
||||
type: 'renew',
|
||||
}],
|
||||
serverStore.inIframe,
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
redeem,
|
||||
purchase,
|
||||
upgrade,
|
||||
renew,
|
||||
};
|
||||
});
|
||||
|
||||
+1
-1
@@ -266,7 +266,7 @@ export const useServerStore = defineStore('server', () => {
|
||||
text: 'Redeem Activation Code',
|
||||
};
|
||||
const renewAction = ref<ServerStateDataAction>({
|
||||
click: () => { purchaseStore.redeem(); },
|
||||
click: () => { purchaseStore.renew(); },
|
||||
external: true,
|
||||
icon: KeyIcon,
|
||||
name: 'renew',
|
||||
|
||||
+32
-1
@@ -111,7 +111,38 @@ export default <Partial<Config>>{
|
||||
DEFAULT: {
|
||||
css: {
|
||||
color: theme('colors.beta'),
|
||||
|
||||
'--tw-prose-body': theme('colors.beta'),
|
||||
'--tw-prose-headings': theme('colors.beta'),
|
||||
'--tw-prose-lead': theme('colors.beta'),
|
||||
'--tw-prose-links': theme('colors.beta'),
|
||||
'--tw-prose-bold': theme('colors.beta'),
|
||||
'--tw-prose-counters': theme('colors.beta'),
|
||||
'--tw-prose-bullets': theme('colors.beta'),
|
||||
'--tw-prose-hr': theme('colors.beta'),
|
||||
'--tw-prose-quotes': theme('colors.beta'),
|
||||
'--tw-prose-quote-borders': theme('colors.beta'),
|
||||
'--tw-prose-captions': theme('colors.beta'),
|
||||
'--tw-prose-code': theme('colors.beta'),
|
||||
'--tw-prose-pre-code': theme('colors.beta'),
|
||||
'--tw-prose-pre-bg': theme('colors.alpha'),
|
||||
'--tw-prose-th-borders': theme('colors.beta'),
|
||||
'--tw-prose-td-borders': theme('colors.beta'),
|
||||
'--tw-prose-invert-body': theme('colors.alpha'),
|
||||
'--tw-prose-invert-headings': theme('colors.alpha'),
|
||||
'--tw-prose-invert-lead': theme('colors.alpha'),
|
||||
'--tw-prose-invert-links': theme('colors.alpha'),
|
||||
'--tw-prose-invert-bold': theme('colors.alpha'),
|
||||
'--tw-prose-invert-counters': theme('colors.alpha'),
|
||||
'--tw-prose-invert-bullets': theme('colors.alpha'),
|
||||
'--tw-prose-invert-hr': theme('colors.alpha'),
|
||||
'--tw-prose-invert-quotes': theme('colors.alpha'),
|
||||
'--tw-prose-invert-quote-borders': theme('colors.alpha'),
|
||||
'--tw-prose-invert-captions': theme('colors.alpha'),
|
||||
'--tw-prose-invert-code': theme('colors.alpha'),
|
||||
'--tw-prose-invert-pre-code': theme('colors.alpha'),
|
||||
'--tw-prose-invert-pre-bg': theme('colors.beta'),
|
||||
'--tw-prose-invert-th-borders': theme('colors.alpha'),
|
||||
'--tw-prose-invert-td-borders': theme('colors.alpha'),
|
||||
// ...
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { Component } from 'vue';
|
||||
|
||||
export interface RegistrationItemProps {
|
||||
component?: Component;
|
||||
componentProps?: object;
|
||||
componentOpacity?: boolean;
|
||||
error?: boolean;
|
||||
label?: string;
|
||||
text?: number | string | undefined;
|
||||
}
|
||||
@@ -32,6 +32,9 @@ export type ServerState = 'BASIC'
|
||||
| 'EBLACKLISTED1'
|
||||
| 'EBLACKLISTED2'
|
||||
| 'ENOCONN'
|
||||
| 'STARTER'
|
||||
| 'UNLEASHED'
|
||||
| 'LIFETIME'
|
||||
| undefined;
|
||||
|
||||
export type ServerconnectPluginInstalled = 'dynamix.unraid.net.plg' | 'dynamix.unraid.net.staging.plg' | 'dynamix.unraid.net.plg_installFailed' | 'dynamix.unraid.net.staging.plg_installFailed' | '';
|
||||
|
||||
Reference in New Issue
Block a user