Files
api/web/components/Registration.ce.vue
Eli Bosley e719780ee8 refactor: enhance component styles and introduce responsive modal
- Updated CSS variables and utility classes for improved theme integration and style consistency across components.
- Introduced a new responsive modal component to enhance user experience on various screen sizes.
- Refined button and badge styles to ensure better visual hierarchy and interaction feedback.
- Adjusted component imports and structure for better modularity and maintainability.
- Removed deprecated styles and streamlined CSS for improved performance and clarity.
2025-09-01 20:06:48 -04:00

394 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts" setup>
/**
* @todo how are we going to update test, beta, and stable releases for internal testing?
* @todo after install / downgrade detect if third-party drivers are installed and tell users to wait for a user to wait for a new notification
*
* run exec("ps aux | grep -E "inotifywait -q /boot/changes.txt -e move_self,delete_self" | grep -v "grep -E inotifywait" | awk '{print $2}'");
* if this returns are value assume we have third-party drivers installed and tell the user to wait for a new notification
*
* view https://s3.amazonaws.com/dnld.lime-technology.com/stable/unRAIDServer.plg to see how the update is handled
# ensure writes to USB flash boot device have completed
sync -f /boot
if [ -z "${plg_update_helper}" ]; then
echo "Update successful - PLEASE REBOOT YOUR SERVER"
else
echo "Third party plugins found - PLEASE CHECK YOUR UNRAID NOTIFICATIONS AND WAIT FOR THE MESSAGE THAT IT IS SAFE TO REBOOT!"
fi
*/
import { computed, onBeforeMount, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { ShieldCheckIcon, ShieldExclamationIcon } from '@heroicons/vue/24/solid';
import { BrandButton, CardWrapper, PageContainer, SettingsGrid } from '@unraid/ui';
import type { RegistrationItemProps } from '~/types/registration';
import type { ServerStateDataAction } from '~/types/server';
import KeyActions from '~/components/KeyActions.vue';
import RegistrationKeyLinkedStatus from '~/components/Registration/KeyLinkedStatus.vue';
import RegistrationReplaceCheck from '~/components/Registration/ReplaceCheck.vue';
import RegistrationUpdateExpirationAction from '~/components/Registration/UpdateExpirationAction.vue';
import UserProfileUptimeExpire from '~/components/UserProfile/UptimeExpire.vue';
import useDateTimeHelper from '~/composables/dateTime';
import { useReplaceRenewStore } from '~/store/replaceRenew';
import { useServerStore } from '~/store/server';
const { t } = useI18n();
const replaceRenewCheckStore = useReplaceRenewStore();
const serverStore = useServerStore();
const {
computedArray,
arrayWarning,
authAction,
dateTimeFormat,
deviceCount,
guid,
flashVendor,
flashProduct,
keyActions,
keyfile,
computedRegDevs,
regGuid,
regTm,
regTo,
regTy,
regExp,
regUpdatesExpired,
serverErrors,
state,
stateData,
stateDataError,
tooManyDevices,
} = storeToRefs(serverStore);
const formattedRegTm = ref<string>();
/**
* regTm may not have a value until we get a response from the refreshServerState action
* So we need to watch for this value to be able to format it based on the user's date time preferences.
*/
const setFormattedRegTm = () => {
if (!regTm.value) {
return;
}
const { outputDateTimeFormatted } = useDateTimeHelper(dateTimeFormat.value, t, true, regTm.value);
formattedRegTm.value = outputDateTimeFormatted.value;
};
watch(regTm, (_newV) => {
setFormattedRegTm();
});
onBeforeMount(() => {
setFormattedRegTm();
/** automatically check for replacement and renewal eligibility…will prompt user if eligible for a renewal / key re-roll for legacy keys */
if (guid.value && keyfile.value) {
replaceRenewCheckStore.check();
}
});
const headingIcon = computed(() =>
serverErrors.value.length ? ShieldExclamationIcon : ShieldCheckIcon
);
const heading = computed(() => {
if (serverErrors.value.length) {
// It's rare to have multiple errors but for the time being only show the first error
return serverErrors.value[0]?.heading;
}
return stateData.value.heading;
});
const subheading = computed(() => {
if (serverErrors.value.length) {
// It's rare to have multiple errors but for the time being only show the first error
return serverErrors.value[0]?.message;
}
return stateData.value.message;
});
const showTrialExpiration = computed(
(): boolean => state.value === 'TRIAL' || state.value === 'EEXPIRED'
);
const showUpdateEligibility = computed((): boolean => !!regExp.value);
const keyInstalled = computed((): boolean => !!(!stateDataError.value && state.value !== 'ENOKEYFILE'));
const showLinkedAndTransferStatus = computed(
(): boolean => !!(keyInstalled.value && guid.value && !showTrialExpiration.value)
);
// filter out renew action and only display other key actions…renew is displayed in RegistrationUpdateExpirationAction
const showFilteredKeyActions = computed(
(): boolean =>
!!(
keyActions.value &&
keyActions.value?.filter((action: ServerStateDataAction) => !['renew'].includes(action.name))
.length > 0
)
);
// Organize items into three sections
const flashDriveItems = computed((): RegistrationItemProps[] => {
return [
...(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,
},
]
: []),
...(state.value === 'EGUID'
? [
{
label: t('Registered GUID'),
text: regGuid.value,
},
]
: []),
];
});
const licenseItems = computed((): RegistrationItemProps[] => {
return [
...(computedArray.value
? [
{
label: t('Array status'),
text: computedArray.value,
warning: arrayWarning.value,
},
]
: []),
...(regTy.value
? [
{
label: t('License key type'),
text: regTy.value,
},
]
: []),
...(regTo.value
? [
{
label: t('Registered to'),
text: regTo.value,
},
]
: []),
...(regTo.value && regTm.value && formattedRegTm.value
? [
{
label: t('Registered on'),
text: formattedRegTm.value,
},
]
: []),
...(showTrialExpiration.value
? [
{
error: state.value === 'EEXPIRED',
label: t('Trial expiration'),
component: UserProfileUptimeExpire,
componentProps: {
forExpire: true,
shortText: true,
t,
},
componentOpacity: true,
},
]
: []),
...(showUpdateEligibility.value
? [
{
label: t('OS Update Eligibility'),
warning: regUpdatesExpired.value,
component: RegistrationUpdateExpirationAction,
componentProps: { t },
componentOpacity: !regUpdatesExpired.value,
},
]
: []),
...(keyInstalled.value
? [
{
error: tooManyDevices.value,
label: t('Attached Storage Devices'),
text: tooManyDevices.value
? t('{0} out of {1} allowed devices upgrade your key to support more devices', [
deviceCount.value,
computedRegDevs.value,
])
: t('{0} out of {1} devices', [
deviceCount.value,
computedRegDevs.value === -1 ? t('unlimited') : computedRegDevs.value,
]),
},
]
: []),
];
});
const actionItems = computed((): RegistrationItemProps[] => {
return [
...(showLinkedAndTransferStatus.value
? [
{
label: t('Transfer License to New Flash'),
component: RegistrationReplaceCheck,
componentProps: { t },
},
]
: []),
...(regTo.value && showLinkedAndTransferStatus.value
? [
{
label: t('Linked to Unraid.net account'),
component: RegistrationKeyLinkedStatus,
componentProps: { t },
},
]
: []),
...(showFilteredKeyActions.value
? [
{
component: KeyActions,
componentProps: {
filterOut: ['renew'],
t,
},
},
]
: []),
];
});
</script>
<template>
<div>
<PageContainer class="max-w-[800px]">
<CardWrapper :increased-padding="true">
<div class="flex flex-col gap-5 sm:gap-6">
<header class="flex flex-col gap-y-4">
<h3
class="text-xl md:text-2xl font-semibold leading-normal flex flex-row items-center gap-2"
:class="serverErrors.length ? 'text-unraid-red' : 'text-green-500'"
>
<component :is="headingIcon" class="w-6 h-6" />
<span>
{{ heading }}
</span>
</h3>
<div
v-if="subheading"
class="prose text-base leading-relaxed whitespace-normal opacity-75"
v-html="subheading"
/>
<span v-if="authAction" class="grow-0">
<BrandButton
:disabled="authAction?.disabled"
:icon="authAction.icon"
:text="t(authAction.text)"
:title="authAction.title ? t(authAction.title) : undefined"
@click="authAction.click?.()"
/>
</span>
</header>
<!-- Flash Drive Section -->
<div v-if="flashDriveItems.length > 0" class="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<h4 class="text-lg font-semibold mb-3">{{ t('Flash Drive') }}</h4>
<SettingsGrid>
<template v-for="item in flashDriveItems" :key="item.label">
<div class="font-semibold flex items-center gap-x-2">
<ShieldExclamationIcon v-if="item.error" class="w-4 h-4 text-unraid-red" />
<span v-html="item.label" />
</div>
<div class="select-all" :class="[item.error ? 'text-unraid-red' : 'opacity-75']">
{{ item.text }}
</div>
</template>
</SettingsGrid>
</div>
<!-- License Section -->
<div v-if="licenseItems.length > 0" class="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<h4 class="text-lg font-semibold mb-3">{{ t('License') }}</h4>
<SettingsGrid>
<template v-for="item in licenseItems" :key="item.label">
<div class="font-semibold flex items-center gap-x-2">
<ShieldExclamationIcon v-if="item.error" class="w-4 h-4 text-unraid-red" />
<span v-html="item.label" />
</div>
<div
:class="[
item.error ? 'text-unraid-red' : item.warning ? 'text-yellow-600' : '',
item.text && !item.error && !item.warning ? 'opacity-75' : ''
]">
<span v-if="item.text" class="select-all">
{{ item.text }}
</span>
<component
:is="item.component"
v-if="item.component"
v-bind="item.componentProps"
:class="[item.componentOpacity && !item.error ? 'opacity-75' : '']"
/>
</div>
</template>
</SettingsGrid>
</div>
<!-- Actions Section -->
<div v-if="actionItems.length > 0" class="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<h4 class="text-lg font-semibold mb-3">{{ t('Actions') }}</h4>
<SettingsGrid>
<template v-for="item in actionItems" :key="item.label || 'action-' + actionItems.indexOf(item)">
<template v-if="item.label">
<div class="font-semibold flex items-center gap-x-2">
<ShieldExclamationIcon v-if="item.error" class="w-4 h-4 text-unraid-red" />
<span v-html="item.label" />
</div>
<div :class="[item.error ? 'text-unraid-red' : '']">
<span v-if="item.text" class="select-all opacity-75">
{{ item.text }}
</span>
<component
:is="item.component"
v-if="item.component"
v-bind="item.componentProps"
:class="[item.componentOpacity && !item.error ? 'opacity-75' : '']"
/>
</div>
</template>
<template v-else>
<div class="md:col-span-2">
<component
:is="item.component"
v-bind="item.componentProps"
/>
</div>
</template>
</template>
</SettingsGrid>
</div>
</div>
</CardWrapper>
</PageContainer>
</div>
</template>