Files
api/web/components/UpdateOs/Update.vue
Eli Bosley 345e83bfb0 feat: upgrade nuxt-custom-elements (#1461)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added new modal dialogs and UI components, including activation steps,
OS update feedback, and expanded notification management.
* Introduced a plugin to configure internationalization, state
management, and Apollo client support in web components.
* Added a new Log Viewer page with a streamlined interface for viewing
logs.

* **Improvements**
* Centralized Pinia state management by consolidating all stores to use
a shared global Pinia instance.
* Simplified component templates by removing redundant
internationalization host wrappers.
* Enhanced ESLint configuration with stricter rules and global variable
declarations.
* Refined custom element build process to prevent jQuery conflicts and
optimize minification.
* Updated component imports and templates for consistent structure and
maintainability.
* Streamlined log viewer dropdowns using simplified select components
with improved formatting.
* Improved notification sidebar with filtering by importance and modular
components.
* Replaced legacy notification popups with new UI components and added
automatic root session creation for localhost requests.
* Updated OS version display and user profile UI with refined styling
and component usage.

* **Bug Fixes**
* Fixed component tag capitalization and improved type annotations
across components.

* **Chores**
* Updated development dependencies including ESLint plugins and build
tools.
* Removed deprecated log viewer patch class and cleaned up related test
fixtures.
  * Removed unused imports and simplified Apollo client setup.
* Cleaned up test mocks and removed obsolete i18n host component tests.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210730229632804

---------

Co-authored-by: Pujit Mehrotra <pujit@lime-technology.com>
Co-authored-by: Zack Spear <zackspear@users.noreply.github.com>
2025-07-08 10:05:39 -04:00

272 lines
10 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 require keyfile to be set before allowing user to check for OS updates
* @todo require keyfile to update
* @todo require valid guid / server state to update
*/
import { computed, ref, watchEffect } from 'vue';
import { storeToRefs } from 'pinia';
import {
ArchiveBoxArrowDownIcon,
ArrowPathIcon,
ArrowTopRightOnSquareIcon,
BellAlertIcon,
EyeIcon,
} from '@heroicons/vue/24/solid';
import { BrandButton, CardWrapper } from '@unraid/ui';
import { Switch, SwitchGroup, SwitchLabel } from '@headlessui/vue';
import dayjs from 'dayjs';
import type { UserProfileLink } from '~/types/userProfile';
import type { ComposerTranslation } from 'vue-i18n';
import useDateTimeHelper from '~/composables/dateTime';
import { useServerStore } from '~/store/server';
import { useUpdateOsStore } from '~/store/updateOs';
import { useUpdateOsActionsStore } from '~/store/updateOsActions';
const props = defineProps<{
t: ComposerTranslation;
}>();
const serverStore = useServerStore();
const { dateTimeFormat, updateOsResponse } = storeToRefs(serverStore);
const updateOsStore = useUpdateOsStore();
const updateOsActionsStore = useUpdateOsActionsStore();
const { connectPluginInstalled, flashBackupActivated } = storeToRefs(useServerStore());
const { available } = storeToRefs(updateOsStore);
const { outputDateTimeFormatted: formattedReleaseDate } = useDateTimeHelper(
dateTimeFormat.value,
props.t,
true,
dayjs(updateOsResponse.value?.date ?? '', 'YYYY-MM-DD').valueOf()
);
const updateButton = ref<UserProfileLink | undefined>();
const heading = computed(() => {
if (available.value && updateButton?.value?.text && updateButton?.value?.textParams) {
return props.t(updateButton?.value.text, updateButton?.value.textParams);
}
return props.t('Check for OS Updates');
});
const headingIcon = computed(() => {
if (available.value) {
return BellAlertIcon;
}
return ArrowPathIcon;
});
const flashBackupCopy = computed(() => {
const base = props.t('We recommend backing up your USB Flash Boot Device before starting the update.');
if (connectPluginInstalled.value && flashBackupActivated.value) {
return `${base}
${props.t('You have already activated the Flash Backup feature via the Unraid Connect plugin.')}
${props.t('Go to Tools > Management Access to ensure your backup is up-to-date.')}
${props.t('You can also manually create a new backup by clicking the Create Flash Backup button.')}
`;
}
if (connectPluginInstalled.value && !flashBackupActivated.value) {
return `${base}
${props.t('You have not activated the Flash Backup feature via the Unraid Connect plugin.')}
${props.t('Go to Tools > Management Access to activate the Flash Backup feature and ensure your backup is up-to-date.')}
${props.t('You can also manually create a new backup by clicking the Create Flash Backup button.')}
`;
}
return `${base} ${props.t('You can manually create a backup by clicking the Create Flash Backup button.')}`;
});
const acknowledgeBackup = ref<boolean>(false);
const flashBackupBasicStatus = ref<'complete' | 'ready' | 'started'>('ready');
const flashBackupText = computed(() => props.t('Create Flash Backup'));
const startFlashBackup = () => {
console.debug('[startFlashBackup]', Date.now());
// @ts-expect-error global function provided by the webgui on the update page
if (typeof flashBackup === 'function') {
flashBackupBasicStatus.value = 'started';
// @ts-expect-error global function provided by the webgui on the update page
flashBackup();
checkFlashBackupStatus();
} else {
alert(
props.t(
'Flash Backup is not available. Navigate to {0}/Main/Settings/Flash to try again then come back to this page.',
[window.location.origin]
)
);
}
};
/**
* Checking for element on the page to determine if the flash backup has started
*/
const checkFlashBackupStatus = () => {
const loadingElement: HTMLCollectionOf<Element> = document.getElementsByClassName('spinner');
setTimeout(() => {
if (loadingElement.length > 0 && loadingElement[0]) {
const el = loadingElement[0] as HTMLDivElement;
const loaderHidden = el.style.display === 'none';
if (loaderHidden) {
flashBackupBasicStatus.value = 'complete';
console.debug('[checkFlashBackupStatus] complete', Date.now());
} else {
checkFlashBackupStatus(); // check again
}
} else {
flashBackupBasicStatus.value = 'complete';
}
}, 500);
};
const disableCallbackButton = computed(
() => !acknowledgeBackup.value || flashBackupBasicStatus.value === 'started'
);
watchEffect(() => {
if (available.value) {
updateButton.value = updateOsActionsStore.updateCallbackButton();
} else {
updateButton.value = updateOsActionsStore.updateCallbackButton();
}
if (flashBackupBasicStatus.value === 'complete') {
acknowledgeBackup.value = true; // auto check the box
}
});
</script>
<template>
<CardWrapper :increased-padding="true">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-20px sm:gap-24px">
<div class="grid gap-y-16px">
<h3 class="font-semibold leading-normal flex flex-row items-start justify-start gap-8px">
<component :is="headingIcon" class="w-20px shrink-0" />
<span class="leading-none inline-flex flex-wrap justify-start items-baseline gap-8px">
<span class="text-20px">
{{ heading }}
</span>
<span v-if="updateOsResponse && formattedReleaseDate" class="text-16px opacity-75 shrink">
{{ formattedReleaseDate }}
</span>
</span>
</h3>
<div class="prose opacity-75 text-16px leading-relaxed whitespace-normal">
<p>
{{
t(
'Receive the latest and greatest for Unraid OS. Whether it new features, security patches, or bug fixes keeping your server up-to-date ensures the best experience that Unraid has to offer.'
)
}}
</p>
<p v-if="available">
{{ flashBackupCopy }}
</p>
</div>
</div>
<div class="flex flex-col sm:flex-shrink-0 items-center gap-16px">
<template v-if="available && updateButton">
<BrandButton
variant="outline"
:disabled="flashBackupBasicStatus === 'started'"
:icon="ArchiveBoxArrowDownIcon"
:name="'flashBackup'"
:text="flashBackupText"
class="flex-none"
@click="startFlashBackup"
/>
<p v-if="flashBackupBasicStatus === 'started'" class="text-12px italic opacity-75 shrink">
{{ t('Backing up...this may take a few minutes') }}
</p>
<SwitchGroup as="div">
<div class="flex flex-shrink-0 items-center gap-16px">
<Switch
v-model="acknowledgeBackup"
:disabled="flashBackupBasicStatus === 'started'"
:class="[
acknowledgeBackup ? 'bg-green-500' : 'bg-gray-200',
'relative inline-flex h-24px w-[44px] 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
:class="[
acknowledgeBackup ? 'translate-x-20px' : 'translate-x-0',
'pointer-events-none relative inline-block h-20px w-20px transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
]"
>
<span
:class="[
acknowledgeBackup
? 'opacity-0 duration-100 ease-out'
: 'opacity-100 duration-200 ease-in',
'absolute inset-0 flex h-full w-full items-center justify-center transition-opacity',
]"
aria-hidden="true"
>
<svg class="h-12px w-12px text-gray-400" fill="none" viewBox="0 0 12 12">
<path
d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</span>
<span
:class="[
acknowledgeBackup
? 'opacity-100 duration-200 ease-in'
: 'opacity-0 duration-100 ease-out',
'absolute inset-0 flex h-full w-full items-center justify-center transition-opacity',
]"
aria-hidden="true"
>
<svg class="h-12px w-12px text-green-500" fill="currentColor" viewBox="0 0 12 12">
<path
d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z"
/>
</svg>
</span>
</span>
</Switch>
<SwitchLabel class="text-14px">
{{ t('I have made a Flash Backup') }}
</SwitchLabel>
</div>
</SwitchGroup>
</template>
<BrandButton
variant="fill"
:disabled="disableCallbackButton"
:external="updateButton?.external"
:icon="EyeIcon"
:icon-right="ArrowTopRightOnSquareIcon"
:name="updateButton?.name"
:text="t('View Available Updates')"
:title="
!acknowledgeBackup
? t('Acklowledge that you have made a Flash Backup to enable this action')
: ''
"
class="flex-none"
@click="updateButton?.click"
/>
</div>
</div>
</CardWrapper>
</template>
<style lang="postcss">
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';
</style>