feat: WIP first pass at UpdateOs page replacement component

This commit is contained in:
Zack Spear
2023-09-20 17:55:03 -07:00
committed by Zack Spear
parent 5c58a86d86
commit 57efcef072
20 changed files with 602 additions and 163 deletions

View File

@@ -43,32 +43,32 @@ const testOsReleasesResponse: OsReleasesResponse = {
}
],
"stable": [
{
"version": "6.12.5",
"name": "Unraid Server 6.12.5",
"basefile": "unRAIDServer-6.12.5-x86_64.zip",
"date": "2023-08-31",
"url": "https://dl.stable.unraid.net/unRAIDServer-6.12.5-x86_64.zip",
"changelog": "https://unraid.net/blog/unraid-os-6.12.5-release-notes",
"md5": "FAKEbddcf415f2d0518804e551c16125",
"size": 12345122,
"sha256": "fda177bb1336270b24e4df0fd0c1dd0596c44699204f57c83ce70a0f19173be4",
"plugin_url": "https://dl.stable.unraid.net/unRAIDServer-6.12.5.plg",
"plugin_sha256": "83850536ed6982bd582ed107d977d59e9b9b786363e698b14d1daf52e2dec2d9"
},
{
"version": "6.12.4",
"name": "Unraid Server 6.12.4",
"basefile": "unRAIDServer-6.12.4-x86_64.zip",
"date": "2023-08-31",
"url": "https://dl.stable.unraid.net/unRAIDServer-6.12.4-x86_64.zip",
"changelog": "https://unraid.net/blog/unraid-os-6.12.4-release-notes",
"md5": "9050bddcf415f2d0518804e551c1be98",
"size": 12345122,
"sha256": "fda177bb1336270b24e4df0fd0c1dd0596c44699204f57c83ce70a0f19173be4",
"plugin_url": "https://dl.stable.unraid.net/unRAIDServer-6.12.4.plg",
"plugin_sha256": "83850536ed6982bd582ed107d977d59e9b9b786363e698b14d1daf52e2dec2d9"
},
// {
// "version": "6.12.5",
// "name": "Unraid Server 6.12.5",
// "basefile": "unRAIDServer-6.12.5-x86_64.zip",
// "date": "2023-08-31",
// "url": "https://dl.stable.unraid.net/unRAIDServer-6.12.5-x86_64.zip",
// "changelog": "https://unraid.net/blog/unraid-os-6.12.5-release-notes",
// "md5": "FAKEbddcf415f2d0518804e551c16125",
// "size": 12345122,
// "sha256": "fda177bb1336270b24e4df0fd0c1dd0596c44699204f57c83ce70a0f19173be4",
// "plugin_url": "https://dl.stable.unraid.net/unRAIDServer-6.12.5.plg",
// "plugin_sha256": "83850536ed6982bd582ed107d977d59e9b9b786363e698b14d1daf52e2dec2d9"
// },
// {
// "version": "6.12.4",
// "name": "Unraid Server 6.12.4",
// "basefile": "unRAIDServer-6.12.4-x86_64.zip",
// "date": "2023-08-31",
// "url": "https://dl.stable.unraid.net/unRAIDServer-6.12.4-x86_64.zip",
// "changelog": "https://unraid.net/blog/unraid-os-6.12.4-release-notes",
// "md5": "9050bddcf415f2d0518804e551c1be98",
// "size": 12345122,
// "sha256": "fda177bb1336270b24e4df0fd0c1dd0596c44699204f57c83ce70a0f19173be4",
// "plugin_url": "https://dl.stable.unraid.net/unRAIDServer-6.12.4.plg",
// "plugin_sha256": "83850536ed6982bd582ed107d977d59e9b9b786363e698b14d1daf52e2dec2d9"
// },
{
"version": "6.12.3",
"name": "Unraid Server 6.12.3",

View File

@@ -70,7 +70,7 @@ export const serverState: Server = {
bgColor: '',
descriptionShow: true,
metaColor: '',
name: 'black',
name: 'white',
textColor: ''
},
uptime,

View File

@@ -30,11 +30,11 @@ defineEmits(['click']);
const classes = computed(() => {
switch (props.btnStyle) {
case 'fill':
return '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';
return 'text-white bg-gradient-to-r from-unraid-red to-orange shadow-none hover:from-unraid-red/60 hover:to-orange/60 focus:from-unraid-red/60 focus:to-orange/60 hover:shadow-md focus:shadow-md';
case 'outline':
return 'text-orange bg-gradient-to-r from-transparent to-transparent border-2 border-solid border-orange hover:text-white focus:text-white hover:from-unraid-red hover:to-orange focus:from-unraid-red focus:to-orange hover:border-transparent focus:border-transparent';
return 'text-orange bg-gradient-to-r from-transparent to-transparent border-2 border-solid border-orange shadow-none hover:text-white focus:text-white hover:from-unraid-red hover:to-orange focus:from-unraid-red focus:to-orange hover:shadow-md focus:shadow-md';
case 'underline':
return 'opacity-75 hover:opacity-100 focus:opacity-100 underline transition hover:text-alpha hover:bg-beta focus:text-alpha focus:bg-beta';
return 'opacity-75 hover:opacity-100 focus:opacity-100 underline transition shadow-none hover:text-alpha hover:bg-beta focus:text-alpha focus:bg-beta hover:shadow-md focus:shadow-md';
}
});
</script>

View File

@@ -45,3 +45,9 @@ provide(I18nInjectionKey, i18n);
<template>
<slot />
</template>
<style>
/* unraid-i18n-host {
font-size: 16px;
} */
</style>

109
web/components/Ui/Badge.vue Normal file
View File

@@ -0,0 +1,109 @@
<script setup lang="ts">
import { XCircleIcon } from '@heroicons/vue/24/solid';
import BrandLoading from '~/components/Brand/Loading.vue';
import BrandLoadingWhite from '~/components/Brand/LoadingWhite.vue';
const props = withDefaults(defineProps<{
color?: 'gray' | 'red' | 'yellow' | 'green' | 'blue' | 'indigo' | 'purple' | 'pink' | 'orange' | 'black' | 'white' | 'transparent' | 'current';
icon?: typeof XCircleIcon | typeof BrandLoading | typeof BrandLoadingWhite;
iconRight?: typeof XCircleIcon | typeof BrandLoading | typeof BrandLoadingWhite;
size?: '12px' | '14px' | '16px' | '18px' | '20px' | '24px';
}>(), {
color: 'gray',
icon: undefined,
iconRight: undefined,
size: '16px',
});
const computedStyleClasses = computed(() => {
let colorClasses = '';
let textSize = '';
let iconSize = '';
switch (props.color) {
case 'red':
colorClasses = 'bg-unraid-red text-white group-hover:bg-orange-dark group-focus:bg-orange-dark';
break;
case 'yellow':
colorClasses = 'bg-yellow-100 text-yellow-800 group-hover:bg-yellow-200 group-focus:bg-yellow-200';
break;
case 'green':
colorClasses = 'bg-green-100 text-green-800 group-hover:bg-green-200 group-focus:bg-green-200';
break;
case 'blue':
colorClasses = 'bg-blue-100 text-blue-800 group-hover:bg-blue-200 group-focus:bg-blue-200';
break;
case 'indigo':
colorClasses = 'bg-indigo-100 text-indigo-800 group-hover:bg-indigo-200 group-focus:bg-indigo-200';
break;
case 'purple':
colorClasses = 'bg-purple-100 text-purple-800 group-hover:bg-purple-200 group-focus:bg-purple-200';
break;
case 'pink':
colorClasses = 'bg-pink-100 text-pink-800 group-hover:bg-pink-200 group-focus:bg-pink-200';
break;
case 'orange':
colorClasses = 'bg-orange text-white group-hover:bg-orange-dark group-focus:bg-orange-dark';
break;
case 'black':
colorClasses = 'bg-black text-white group-hover:bg-gray-800 group-focus:bg-gray-800';
break;
case 'white':
colorClasses = 'bg-white text-black group-hover:bg-gray-100 group-focus:bg-gray-100';
break;
case 'transparent':
colorClasses = 'bg-transparent text-black group-hover:bg-gray-100 group-focus:bg-gray-100';
break;
case 'current':
colorClasses = 'bg-current text-black group-hover:bg-gray-100 group-focus:bg-gray-100';
break;
case 'gray':
colorClasses = 'bg-gray-200 text-gray-800 group-hover:bg-gray-300 group-focus:bg-gray-300';
break;
}
switch (props.size) {
case '12px':
textSize = 'text-12px px-8px py-4px';
iconSize = 'w-12px';
break;
case '14px':
textSize = 'text-14px px-8px py-4px';
iconSize = 'w-14px';
break;
case '16px':
textSize = 'text-16px px-12px py-8px';
iconSize = 'w-16px';
break;
case '18px':
textSize = 'text-18px px-12px py-8px';
iconSize = 'w-18px';
break;
case '20px':
textSize = 'text-20px px-16px py-12px';
iconSize = 'w-20px';
break;
case '24px':
textSize = 'text-24px px-16px py-12px';
iconSize = 'w-24px';
break;
}
return {
badge: `${textSize} ${colorClasses}`,
icon: iconSize,
};
});
</script>
<template>
<span
class="inline-flex items-center rounded-full font-semibold leading-none transition-all duration-200 ease-in-out"
:class="[
computedStyleClasses.badge,
icon || iconRight ? 'gap-8px' : '',
]"
>
<component :is="icon" v-if="icon" class="flex-shrink-0" :class="computedStyleClasses.icon" />
<slot></slot>
<component :is="iconRight" v-if="iconRight" class="flex-shrink-0" :class="computedStyleClasses.icon" />
</span>
</template>

View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
withDefaults(defineProps<{ increasedPadding?: boolean }>(), {
increasedPadding: false,
});
</script>
<template>
<div
class="group/card p-4 text-left relative flex flex-col flex-1 text-beta bg-alpha border-2 border-solid border-gamma/50 rounded-md shadow-md hover:shadow-orange/50 transition-all"
:class="increasedPadding && 'md:p-6'"
>
<slot></slot>
</div>
</template>

View File

@@ -3,118 +3,44 @@
* @todo require keyfile to be set before allowing user to check for updates
* @todo require keyfile to update
* @todo require valid guid / server state to update
* @todo detect downgrade possibility
*/
import { Switch, SwitchGroup, SwitchLabel } from '@headlessui/vue'
import { ArrowTopRightOnSquareIcon, BellAlertIcon } from '@heroicons/vue/24/solid';
import dayjs from 'dayjs';
import dayjs, { extend } from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { storeToRefs } from 'pinia';
import { ref, watchEffect } from 'vue';
import { useI18n } from 'vue-i18n';
import { useUpdateOsStore } from '~/store/updateOsActions';
import 'tailwindcss/tailwind.css';
import '~/assets/main.css';
import { useServerStore } from '~/store/server';
import { useUpdateOsStore, useUpdateOsActionsStore } from '~/store/updateOsActions';
import type { UserProfileLink } from '~/types/userProfile';
const { t } = useI18n();
const serverStore = useServerStore();
const updateOsStore = useUpdateOsStore();
const updateOsActionsStore = useUpdateOsActionsStore();
const { guid, keyfile, osVersion } = storeToRefs(serverStore);
const { available, cachedReleasesTimestamp } = storeToRefs(updateOsStore);
const includeNext = ref(false);
const updateButton = ref<UserProfileLink | undefined>();
const parsedCachedReleasesTimestamp = computed(() => {
if (!cachedReleasesTimestamp.value) { return ''; }
return dayjs(cachedReleasesTimestamp.value).format('YYYY-MM-DD HH:mm:ss');
export interface Props {
restoreVersion?: string;
}
withDefaults(defineProps<Props>(), {
restoreVersion: '',
});
const check = () => {
updateOsStore.checkForUpdate({
cache: true,
guid: guid.value,
includeNext: includeNext.value,
keyfile: keyfile.value,
osVersion: osVersion.value,
skipCache: true,
});
};
const updateOsStore = useUpdateOsStore();
const { cachedReleasesTimestamp } = storeToRefs(updateOsStore);
watchEffect(() => {
if (available.value) {
updateButton.value = updateOsActionsStore.initUpdateOsCallback();
} else {
updateButton.value = undefined;
}
extend(relativeTime);
const parsedReleaseTimestamp = computed(() => {
if (!cachedReleasesTimestamp.value) { return ''; }
return {
formatted: dayjs(cachedReleasesTimestamp.value).format('YYYY-MM-DD HH:mm:ss'),
relative: dayjs().to(dayjs(cachedReleasesTimestamp.value)),
};
});
</script>
<template>
<div class="grid gap-y-24px">
<div class="grid gap-y-16px">
<h1 class="text-24px">{{ t('Update Unraid OS') }}</h1>
<p>Current Version: {{ osVersion }}</p>
<p>Status: {{ available ? 'Update Available' : 'Up-to-date' }}</p>
</div>
<div class="text-16px text-alpha bg-beta text-left relative flex flex-col justify-around border-2 border-solid shadow-xl transform overflow-hidden rounded-lg transition-all sm:w-full">
<div class="px-16px py-20px sm:p-24px">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-20px">
<div class="grid gap-y-16px">
<h3 class="text-20px font-semibold leading-6 text-gray-900 flex flex-row items-center gap-8px">
<BellAlertIcon class="w-20px shrink-0" />
<span>
{{ available ? t(updateButton?.text ?? '', updateButton?.textParams) : t('Check for Updates')}}
</span>
</h3>
<div class="max-w-xl text-sm text-gray-500 whitespace-normal">
<p class="text-18px">{{ 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>
</div>
</div>
<div class="flex flex-col sm:flex-shrink-0 items-center gap-16px">
<SwitchGroup v-if="!available" as="div">
<div class="flex flex-shrink-0 items-center gap-16px">
<Switch v-model="includeNext" :class="[includeNext ? 'bg-green-500' : '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 :class="[includeNext ? 'translate-x-5' : 'translate-x-0', 'pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out']">
<span :class="[includeNext ? '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="[includeNext ? '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('Include Prereleases') }}</SwitchLabel>
</div>
</SwitchGroup>
<span class="flex flex-col gap-y-8px">
<BrandButton
v-if="available && updateButton"
@click="updateButton?.click"
:external="updateButton?.external"
:icon-right="ArrowTopRightOnSquareIcon"
:name="updateButton?.name"
:text="t('View changelog & update')" />
<BrandButton v-else @click="check" btn-style="outline" :text="t('Check Now')" />
<span class="text-14px text-gamma text-center">{{ t('Last checked: {0}', [parsedCachedReleasesTimestamp]) }}</span>
</span>
</div>
</div>
</div>
</div>
<div class="grid gap-y-24px max-w-1024px mx-auto">
<UpdateOsStatus :release-check-time="parsedReleaseTimestamp" :t="t" />
<UpdateOsUpdate :release-check-time="parsedReleaseTimestamp" :t="t" />
<UpdateOsDowngrade v-if="restoreVersion" :version="restoreVersion" :t="t" />
</div>
</template>

View File

@@ -0,0 +1,88 @@
<script setup lang="ts">
/**
* @todo require keyfile to be set before allowing user to check for updates
* @todo require keyfile to update
* @todo require valid guid / server state to update
* @todo detect downgrade possibility
*/
import { Switch, SwitchGroup, SwitchLabel } from '@headlessui/vue';
import { storeToRefs } from 'pinia';
import { ref, watchEffect } from 'vue';
import 'tailwindcss/tailwind.css';
import '~/assets/main.css';
import { useServerStore } from '~/store/server';
import { useUpdateOsStore, useUpdateOsActionsStore } from '~/store/updateOsActions';
import type { UserProfileLink } from '~/types/userProfile';
import { stat } from 'fs';
const props = defineProps<{
releaseCheckTime: {
formatted: string;
relative: string;
};
t: any;
}>();
const serverStore = useServerStore();
const updateOsStore = useUpdateOsStore();
const updateOsActionsStore = useUpdateOsActionsStore();
const { guid, keyfile, osVersion } = storeToRefs(serverStore);
const { available } = storeToRefs(updateOsStore);
const includeNext = ref(false);
const status = ref<'ready' | 'checking'>('ready');
const buttonText = computed(() => {
if (status.value === 'checking') {
return props.t('Checking...');
}
return props.t('Check For Updates');
});
const check = async () => {
status.value = 'checking';
await updateOsStore.checkForUpdate({
cache: true,
guid: guid.value,
includeNext: includeNext.value,
keyfile: keyfile.value,
osVersion: osVersion.value,
skipCache: true,
}).finally(() => {
status.value = 'ready';
})
};
</script>
<template>
<div class="flex flex-col sm:flex-shrink-0 items-center gap-16px">
<SwitchGroup as="div">
<div class="flex flex-shrink-0 items-center gap-16px">
<Switch v-model="includeNext" :class="[includeNext ? '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="[includeNext ? '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="[includeNext ? '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="[includeNext ? '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('Include Prereleases') }}</SwitchLabel>
</div>
</SwitchGroup>
<span class="flex flex-col gap-y-8px">
<BrandButton @click="check" :disabled="status === 'checking'" btn-style="outline" :text="buttonText" class="flex-0" />
<span class="text-14px opacity-75 text-center" :title="releaseCheckTime.formatted">{{ t('Last checked: {0}', [releaseCheckTime.relative]) }}</span>
</span>
</div>
</template>

View File

@@ -0,0 +1,64 @@
<script lang="ts" setup>
import { ArrowUturnDownIcon, InformationCircleIcon } from '@heroicons/vue/24/solid';
import type { SemVer } from 'semver';
import { ref } from 'vue';
import 'tailwindcss/tailwind.css';
import '~/assets/main.css';
import { useUpdateOsActionsStore } from '~/store/updateOsActions';
import type { UserProfileLink } from '~/types/userProfile';
const props = defineProps<{
t: any;
version: string;
}>();
const updateOsActionsStore = useUpdateOsActionsStore();
const downgradeButton = ref<UserProfileLink | undefined>({
click: () => {
// @ts-ignore global function provided by the webgui on the update page
downgrade();
},
name: 'downgrade',
text: props.t('Begin restore to Unraid {0}', [props.version]),
});
</script>
<template>
<UiCardWrapper :increased-padding="true">
<div class="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-20px sm:gap-24px">
<div class="grid gap-y-16px">
<h3 class="text-20px font-semibold leading-6 flex flex-row items-center gap-8px">
<ArrowUturnDownIcon class="w-20px shrink-0" />
{{ t('Downgrade Unraid OS to {0}', [version]) }}
</h3>
<div class="text-16px leading-relaxed opacity-75 whitespace-normal">
<p>{{ t('Downgrades are only recommended if you\'re unable to solve a critical issue. In the rare event you need to downgrade we ask that you please provide us with Diagnostics so we can investigate your issue. You will be prompted with the option download the Diagnostics zip once the downgrade process is started. From there please open a bug report on our forums.') }}</p>
</div>
</div>
<div v-if="downgradeButton" class="flex flex-col sm:flex-shrink-0 items-center gap-16px">
<BrandButton
@click="downgradeButton?.click"
btn-style="underline"
:icon="InformationCircleIcon"
:text="t('View Changelog for {0}', [version])" />
<BrandButton
@click="downgradeButton?.click"
btn-style="outline"
:external="downgradeButton?.external"
:icon="ArrowUturnDownIcon"
:name="downgradeButton?.name"
:text="downgradeButton?.text" />
</div>
</div>
</UiCardWrapper>
</template>
<style lang="postcss">
@tailwind base;
@tailwind components;
@tailwind utilities;
</style>

View File

@@ -0,0 +1,66 @@
<script lang="ts" setup>
import {
BellAlertIcon,
CheckCircleIcon,
InformationCircleIcon,
} from '@heroicons/vue/24/solid';
import { storeToRefs } from 'pinia';
import { useServerStore } from '~/store/server';
import { useUpdateOsStore } from '~/store/updateOsActions';
const props = defineProps<{
releaseCheckTime: {
formatted: string;
relative: string;
};
t: any;
}>();
const serverStore = useServerStore();
const updateOsStore = useUpdateOsStore();
const { guid, keyfile, osVersion } = storeToRefs(serverStore);
const { available } = storeToRefs(updateOsStore);
const viewReleaseNotes = () => {
// @ts-ignore  this is a global function provided by the webgui
if (typeof openChanges === 'function') {
// @ts-ignore
openChanges(
'showchanges /var/tmp/unRAIDServer.txt',
props.t('{0} Release Notes', [osVersion.value]),
);
} else {
alert('Unable to open release notes');
}
};
</script>
<template>
<div class="grid gap-y-16px">
<h1 class="text-24px">{{ t('Update Unraid OS') }}</h1>
<div class="flex flex-col md:flex-row gap-16px justify-start md:items-start md:justify-between">
<div class="inline-flex gap-8px">
<button
@click="viewReleaseNotes"
class="group"
:title="t('View changelog for current version {0}', [osVersion])"
>
<UiBadge :icon="InformationCircleIcon">
{{ t('Current Version {0}', [osVersion]) }}
</UiBadge>
</button>
<UiBadge
:color="available ? 'orange' : 'green'"
:icon="available ? BellAlertIcon : CheckCircleIcon"
:title="t('Last checked: {0}', [releaseCheckTime.relative])"
>
{{ available ? 'Update Available' : 'Up-to-date' }}
</UiBadge>
</div>
<UpdateOsCheckButton :releaseCheckTime="releaseCheckTime" :t="t" />
</div>
</div>
</template>

View File

@@ -0,0 +1,102 @@
<script lang="ts" setup>
/**
* @todo require keyfile to be set before allowing user to check for updates
* @todo require keyfile to update
* @todo require valid guid / server state to update
* @todo detect downgrade possibility
*/
import { Switch, SwitchGroup, SwitchLabel } from '@headlessui/vue'
import {
ArrowPathIcon,
ArrowTopRightOnSquareIcon,
BellAlertIcon,
} from '@heroicons/vue/24/solid';
import dayjs from 'dayjs';
import { storeToRefs } from 'pinia';
import { ref, watchEffect } from 'vue';
import 'tailwindcss/tailwind.css';
import '~/assets/main.css';
import { useServerStore } from '~/store/server';
import { useUpdateOsStore, useUpdateOsActionsStore } from '~/store/updateOsActions';
import type { UserProfileLink } from '~/types/userProfile';
const props = defineProps<{
releaseCheckTime: {
formatted: string;
relative: string;
};
t: any;
}>();
const serverStore = useServerStore();
const updateOsStore = useUpdateOsStore();
const updateOsActionsStore = useUpdateOsActionsStore();
const { guid, keyfile, osVersion } = storeToRefs(serverStore);
const { available } = storeToRefs(updateOsStore);
const includeNext = ref(false);
const updateButton = ref<UserProfileLink | undefined>();
const availableText = computed(() => {
if (available.value && updateButton?.value?.text && updateButton?.value?.textParams) {
return props.t(updateButton?.value.text, updateButton?.value.textParams);
}
});
const check = () => {
updateOsStore.checkForUpdate({
cache: true,
guid: guid.value,
includeNext: includeNext.value,
keyfile: keyfile.value,
osVersion: osVersion.value,
skipCache: true,
});
};
watchEffect(() => {
if (available.value) {
updateButton.value = updateOsActionsStore.initUpdateOsCallback();
} else {
updateButton.value = undefined;
}
});
</script>
<template>
<UiCardWrapper :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="text-20px font-semibold leading-6 flex flex-row items-center gap-8px">
<BellAlertIcon v-if="available" class="w-20px shrink-0" />
<ArrowPathIcon v-else class="w-20px shrink-0" />
<span>
{{ availableText ? availableText : t('Check for Updates')}}
</span>
</h3>
<div class="text-16px leading-relaxed whitespace-normal opacity-75">
<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>
</div>
</div>
<BrandButton
v-if="available && updateButton"
@click="updateButton?.click"
:external="updateButton?.external"
:icon-right="ArrowTopRightOnSquareIcon"
:name="updateButton?.name"
:text="t('View changelog & update')" />
</div>
</UiCardWrapper>
</template>
<style lang="postcss">
@tailwind base;
@tailwind components;
@tailwind utilities;
</style>

View File

@@ -36,6 +36,7 @@ const {
connectPluginInstalled,
} = storeToRefs(serverStore);
const { bannerGradient, theme } = storeToRefs(useThemeStore());
const { isOsVersionStable } = storeToRefs(updateOsStore);
const hideDropdown = computed(() => state.value === 'PRO' && !connectPluginInstalled.value);
@@ -84,9 +85,11 @@ onBeforeMount(() => {
}
callbackStore.watcher();
updateOsStore.checkForUpdate({
cache: true,
guid: guid.value,
includeNext: isOsVersionStable.value, // @todo ensure this is correct
keyfile: keyfile.value,
osVersion: osVersion.value,
});

View File

@@ -3,6 +3,7 @@ import { storeToRefs } from 'pinia';
import {
Bars3Icon,
Bars3BottomRightIcon,
BellAlertIcon,
ExclamationTriangleIcon,
InformationCircleIcon,
ShieldExclamationIcon,
@@ -11,6 +12,7 @@ import {
import { useDropdownStore } from '~/store/dropdown';
import { useErrorsStore } from '~/store/errors';
import { useServerStore } from '~/store/server';
import { useUpdateOsStore } from '~/store/updateOsActions';
const props = defineProps<{ t: any; }>();
@@ -18,6 +20,7 @@ const dropdownStore = useDropdownStore();
const { dropdownVisible } = storeToRefs(dropdownStore);
const { errors } = storeToRefs(useErrorsStore());
const { state, stateData } = storeToRefs(useServerStore());
const { available: osUpdateAvailable } = storeToRefs(useUpdateOsStore());
const showErrorIcon = computed(() => errors.value.length || stateData.value.error);
@@ -49,9 +52,16 @@ const title = computed((): string => {
<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>
<BellAlertIcon v-if="osUpdateAvailable" class="hover:animate-pulse text-white fill-current relative w-16px h-16px" />
<Bars3Icon v-if="!dropdownVisible" class="w-20px" />
<Bars3BottomRightIcon v-else class="w-20px" />
<BrandAvatar />
<span class="relative">
<BrandAvatar />
<!-- <span v-if="osUpdateAvailable" class="absolute z-10 -bottom-1 -right-3 w-24px h-24px flex items-center justify-center shadow border border-white bg-gradient-to-r from-unraid-red to-orange rounded-full">
<BellAlertIcon class="hover:animate-pulse text-white fill-current relative w-12px h-12px" />
</span> -->
</span>
</button>
</template>

View File

@@ -208,10 +208,24 @@
"Unraid OS Update Available": "Unraid OS Update Available",
"Update Unraid OS confirmation required": "Update Unraid OS confirmation required",
"Please confirm the update details below": "Please confirm the update details below",
"Current Version: Unraid {0}": "Current Version: Unraid {0}",
"Current Version {0}": "Current Version {0}",
"New Version: {0}": "New Version: {0}",
"This update will require a reboot": "This update will require a reboot",
"Confirm and start update": "Confirm and start update",
"Update Unraid OS": "Update Unraid OS",
"Last checked: {0}": "Last checked: {0}"
"Last checked: {0}": "Last checked: {0}",
"Downgrade Unraid OS": "Downgrade Unraid OS",
"Downgrade Unraid OS to {0}": "Downgrade Unraid OS to {0}",
"Begin restore to Unraid {0}": "Begin restore to Unraid {0}",
"Version available for restore {0}": "Version available for restore {0}",
"Check for Updates": "Check for Updates",
"Include Prereleases": "Include Prereleases",
"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.": "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.",
"Check For Updates": "Check For Updates",
"Checking...": "Checking...",
"View changelog for current version {0}": "View changelog for current version {0}",
"View Changelog for {0}": "View Changelog for {0}",
"View changelog & update": "View changelog & update",
"{0} Release Notes": "{0} Release Notes",
"Unable to open release notes": "Unable to open release notes"
}

12
web/package-lock.json generated
View File

@@ -20,6 +20,7 @@
"graphql-tag": "^2.12.6",
"graphql-ws": "^5.14.0",
"hex-to-rgba": "^2.0.1",
"marked": "^9.0.3",
"semver": "^7.5.4",
"vue-i18n": "^9.2.2",
"wretch": "^2.6.0"
@@ -13269,6 +13270,17 @@
"node": ">=0.10.0"
}
},
"node_modules/marked": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/marked/-/marked-9.0.3.tgz",
"integrity": "sha512-pI/k4nzBG1PEq1J3XFEHxVvjicfjl8rgaMaqclouGSMPhk7Q3Ejb2ZRxx/ZQOcQ1909HzVoWCFYq6oLgtL4BpQ==",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 16"
}
},
"node_modules/mdn-data": {
"version": "2.0.30",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",

View File

@@ -49,6 +49,7 @@
"graphql-tag": "^2.12.6",
"graphql-ws": "^5.14.0",
"hex-to-rgba": "^2.0.1",
"marked": "^9.0.3",
"semver": "^7.5.4",
"vue-i18n": "^9.2.2",
"wretch": "^2.6.0"

View File

@@ -18,7 +18,9 @@ onBeforeMount(() => {
<h3 class="text-lg font-semibold font-mono">
UserProfileCe
</h3>
<UserProfileCe :server="serverState" />
<header class="bg-beta py-4">
<UserProfileCe :server="serverState" />
</header>
<hr class="border-black dark:border-white">
<h3 class="text-lg font-semibold font-mono">
DownloadApiLogsCe
@@ -43,7 +45,7 @@ onBeforeMount(() => {
<h3 class="text-lg font-semibold font-mono">
UpdateOsCe
</h3>
<UpdateOsCe />
<UpdateOsCe :restore-version="'6.11.2'" />
<hr class="border-black dark:border-white">
<h3 class="text-lg font-semibold font-mono">
ModalsCe

View File

@@ -16,7 +16,9 @@ onBeforeMount(() => {
<h3 class="text-lg font-semibold font-mono">
UserProfileCe
</h3>
<unraid-user-profile :server="JSON.stringify(serverState)" />
<header class="bg-beta py-4">
<unraid-user-profile :server="JSON.stringify(serverState)" />
</header>
<hr class="border-black dark:border-white">
<h3 class="text-lg font-semibold font-mono">
DownloadApiLogsCe

View File

@@ -71,6 +71,10 @@ export const useUpdateOsStoreGeneric = (
// getters
const cachedReleasesTimestamp = computed(() => releases.value?.timestamp);
const isOsVersionStable = computed(() => !isVersionStable(osVersion.value));
const isAvailableStable = computed(() => {
if (!available.value) return undefined;
return !isVersionStable(available.value);
});
const filteredStableReleases = computed(() => {
if (!osVersion.value) return undefined;
@@ -106,6 +110,7 @@ export const useUpdateOsStoreGeneric = (
});
// actions
const setReleasesState = (response: ReleasesResponse) => {
console.debug('[setReleasesState]');
releases.value = {
timestamp: Date.now(),
response,
@@ -113,44 +118,64 @@ export const useUpdateOsStoreGeneric = (
}
const cacheReleasesResponse = () => {
console.debug('[cacheReleasesResponse]');
localStorage.setItem(RELEASES_LOCAL_STORAGE_KEY, JSON.stringify(releases.value));
};
const purgeReleasesCache = () => {
const purgeReleasesCache = async () => {
console.debug('[purgeReleasesCache]');
releases.value = undefined;
localStorage.removeItem(RELEASES_LOCAL_STORAGE_KEY);
await localStorage.removeItem(RELEASES_LOCAL_STORAGE_KEY);
};
const requestReleases = async (payload: RequestReleasesPayload): Promise<ReleasesResponse | undefined> => {
console.debug('[requestReleases]', payload);
if (!payload || !payload.guid || !payload.keyfile) {
throw new Error('Invalid Payload for updateOs.requestReleases');
}
if (payload.skipCache) {
purgeReleasesCache();
await purgeReleasesCache();
}
/**
* Compare the timestamp of the cached releases data to the current time,
* if it's older than 7 days, reset releases.
* Which will trigger a new API call to get the releases.
* Otherwise skip the API call and use the cached data.
*/
if (releases.value) {
const currentTime = new Date().getTime();
const cacheDuration = import.meta.env.DEV ? 30000 : 604800000; // 30 seconds for testing, 7 days for prod
if (currentTime - releases.value.timestamp > cacheDuration) {
purgeReleasesCache();
} else {
// if the cache is valid return the existing response
return releases.value.response;
}
}
* Compare the timestamp of the cached releases data to the current time,
* if it's older than 7 days, reset releases.
* Which will trigger a new API call to get the releases.
* Otherwise skip the API call and use the cached data.
*/
else if (!payload.skipCache && releases.value) {
const currentTime = new Date().getTime();
const cacheDuration = import.meta.env.DEV ? 30000 : 604800000; // 30 seconds for testing, 7 days for prod
if (currentTime - releases.value.timestamp > cacheDuration) {
// cache is expired, purge it
console.debug('[requestReleases] cache EXPIRED');
await purgeReleasesCache();
} else {
// if the cache is valid return the existing response
console.debug('[requestReleases] cache VALID');
return releases.value.response;
}
}
// If here we're needing to fetch a new releases…whether it's the first time or b/c the cache was expired
try {
// const response: ReleasesResponse = await request.url(OS_RELEASES.toString()).get().json();
const response: ReleasesResponse = await testReleasesResponse;
console.debug('[requestReleases] fetching new releases', testReleasesResponse);
/**
* @todo replace with real api call, note that the structuredClone is required otherwise Vue will not provided a reactive object from the original static response
* const response: ReleasesResponse = await request.url(OS_RELEASES.toString()).get().json();
*/
const response: ReleasesResponse = await structuredClone(testReleasesResponse);
console.debug('[requestReleases] response', response);
/**
* If we're on stable and the user hasn't requested to include next releases in the check
* then remove next releases from the data
*/
console.debug('[requestReleases] checking for next releases', payload.includeNext, response.next)
if (!payload.includeNext && response.next) {
console.debug('[requestReleases] removing next releases from data')
delete response.next;
}
// save it to local state
setReleasesState(response);
@@ -175,6 +200,9 @@ export const useUpdateOsStoreGeneric = (
// set the osVersion since this is the first thing in this store using it…that way we don't need to import the server store in this store.
osVersion.value = payload.osVersion;
// reset available
available.value = '';
// gets releases from cache or fetches from api
await requestReleases(payload);
@@ -182,16 +210,6 @@ export const useUpdateOsStoreGeneric = (
return console.error('[checkForUpdate] no releases found');
}
/**
* If we're on stable and the user hasn't requested to include next releases in the check
* then remove next releases from the data
*/
console.debug('[checkForUpdate] checking for next releases', payload.includeNext, isOsVersionStable.value, releases.value.response.next)
if (!payload.includeNext || isOsVersionStable.value && releases.value.response.next) {
console.debug('[checkForUpdate] removing next releases from data')
delete releases.value.response.next;
}
Object.keys(releases.value.response ?? {}).forEach(key => {
// this is just to make TS happy (it's already checked above…thanks github copilot for knowing what I needed)
if (!releases.value) {
@@ -247,6 +265,7 @@ export const useUpdateOsStoreGeneric = (
// getters
cachedReleasesTimestamp,
isOsVersionStable,
isAvailableStable,
filteredStableReleases,
filteredNextReleases,
allFilteredReleases,

View File

@@ -96,6 +96,7 @@ export default <Partial<Config>>{
'350px': '350px',
'640px': '640px',
'800px': '800px',
'1024px': '1024px',
},
screens: {
'2xs': '470px',