mirror of
https://github.com/unraid/api.git
synced 2025-12-31 13:39:52 -06:00
feat: move to iframe for changelog (#1388)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Changelog modal now displays changelog documentation within an embedded iframe if a URL is available, allowing navigation within the iframe and providing a "Back to Changelog" button to return to the original view. - "View on Docs" button dynamically updates to reflect the current page within the iframe. - Added support for displaying a formatted changelog string and testing modal behavior with or without this content. - Introduced a new component to fetch, parse, and render changelogs from URLs with enhanced markdown handling. - Update OS store extended to manage changelog display and release stability, consolidating changelog state and actions. - **Bug Fixes** - Close button in modal dialogs is now visible on all screen sizes. - **Chores** - Updated the default server state, which may affect registration device counts and types. - Added new localization string for changelog titles with version placeholders. - Removed deprecated changelog store and related tests, simplifying state management. - Refined backup and restoration scripts for unraid-components directory to use move operations with improved logging. - Improved modal visibility state handling by consolidating changelog modal controls into the main update OS store. - Added URL origin and pattern checks for changelog iframe navigation security. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -134,8 +134,8 @@ done
|
|||||||
# Handle the unraid-components directory
|
# Handle the unraid-components directory
|
||||||
DIR=/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components
|
DIR=/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components
|
||||||
if [ -d "$DIR" ] && [ ! -d "$DIR-" ]; then
|
if [ -d "$DIR" ] && [ ! -d "$DIR-" ]; then
|
||||||
cp -rp "$DIR" "$DIR-"
|
mv "$DIR" "$DIR-"
|
||||||
echo "Backed up directory: $DIR"
|
echo "Moved directory: $DIR to $DIR-"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Backup complete."
|
echo "Backup complete."
|
||||||
@@ -204,12 +204,13 @@ exit 0
|
|||||||
|
|
||||||
# Handle the unraid-components directory
|
# Handle the unraid-components directory
|
||||||
DIR=/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components
|
DIR=/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components
|
||||||
# certain instances where the directory is not present and others where it is, ensure we delete it before we restore it
|
# Remove the archive's contents before restoring
|
||||||
if [ -d "$DIR" ]; then
|
if [ -d "$DIR" ]; then
|
||||||
rm -rf "$DIR"
|
rm -rf "$DIR"
|
||||||
fi
|
fi
|
||||||
if [ -d "$DIR-" ]; then
|
if [ -d "$DIR-" ]; then
|
||||||
mv -f "$DIR-" "$DIR"
|
mv "$DIR-" "$DIR"
|
||||||
|
echo "Restored directory: $DIR- to $DIR"
|
||||||
fi
|
fi
|
||||||
]]>
|
]]>
|
||||||
</INLINE>
|
</INLINE>
|
||||||
|
|||||||
@@ -402,6 +402,7 @@ class WebComponentTranslations
|
|||||||
'Your license key is not eligible for Unraid OS {0}' => sprintf(_('Your license key is not eligible for Unraid OS %s'), '{0}'),
|
'Your license key is not eligible for Unraid OS {0}' => sprintf(_('Your license key is not eligible for Unraid OS %s'), '{0}'),
|
||||||
'Your Trial has expired' => _('Your Trial has expired'),
|
'Your Trial has expired' => _('Your Trial has expired'),
|
||||||
'Your Trial key has been extended!' => _('Your Trial key has been extended!'),
|
'Your Trial key has been extended!' => _('Your Trial key has been extended!'),
|
||||||
|
'Unraid OS {0} Changelog' => sprintf(_('Unraid OS %s Changelog'), '{0}'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ describe('UpdateOs Store', () => {
|
|||||||
describe('State and Getters', () => {
|
describe('State and Getters', () => {
|
||||||
it('should initialize with correct default values', () => {
|
it('should initialize with correct default values', () => {
|
||||||
expect(store.checkForUpdatesLoading).toBe(false);
|
expect(store.checkForUpdatesLoading).toBe(false);
|
||||||
expect(store.modalOpen).toBe(false);
|
expect(store.updateOsModalVisible).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have computed properties with the right types', () => {
|
it('should have computed properties with the right types', () => {
|
||||||
@@ -86,15 +86,15 @@ describe('UpdateOs Store', () => {
|
|||||||
await store.localCheckForUpdate();
|
await store.localCheckForUpdate();
|
||||||
|
|
||||||
expect(WebguiCheckForUpdate).toHaveBeenCalled();
|
expect(WebguiCheckForUpdate).toHaveBeenCalled();
|
||||||
expect(store.modalOpen).toBe(true);
|
expect(store.updateOsModalVisible).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set modal open state', () => {
|
it('should set modal open state', () => {
|
||||||
store.setModalOpen(true);
|
store.setModalOpen(true);
|
||||||
expect(store.modalOpen).toBe(true);
|
expect(store.updateOsModalVisible).toBe(true);
|
||||||
|
|
||||||
store.setModalOpen(false);
|
store.setModalOpen(false);
|
||||||
expect(store.modalOpen).toBe(false);
|
expect(store.updateOsModalVisible).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle errors when checking for updates', async () => {
|
it('should handle errors when checking for updates', async () => {
|
||||||
|
|||||||
@@ -1,197 +0,0 @@
|
|||||||
/**
|
|
||||||
* UpdateOsChangelog store test coverage
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { nextTick } from 'vue';
|
|
||||||
import { createPinia, setActivePinia } from 'pinia';
|
|
||||||
|
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
|
|
||||||
import type { ServerUpdateOsResponse } from '~/types/server';
|
|
||||||
|
|
||||||
import { useUpdateOsChangelogStore } from '~/store/updateOsChangelog';
|
|
||||||
|
|
||||||
vi.mock('~/helpers/markdown', () => ({
|
|
||||||
Markdown: {
|
|
||||||
create: () => ({
|
|
||||||
setOptions: vi.fn(),
|
|
||||||
parse: vi.fn().mockResolvedValue('<h1>Test Title</h1><p>Test content</p>'),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock('~/helpers/urls', () => ({
|
|
||||||
DOCS_RELEASE_NOTES: {
|
|
||||||
toString: () => 'https://docs.unraid.net/unraid-os/release-notes/',
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock('marked-base-url', () => ({
|
|
||||||
baseUrl: vi.fn().mockReturnValue(vi.fn()),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock('semver/functions/prerelease', () => ({
|
|
||||||
default: vi.fn((version) => (version && version.includes('-') ? ['beta', '1'] : null)),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const mockRequestText = vi.fn().mockResolvedValue('# Test Changelog\n\nTest content');
|
|
||||||
vi.mock('~/composables/services/request', () => ({
|
|
||||||
request: {
|
|
||||||
url: () => ({
|
|
||||||
get: () => ({
|
|
||||||
text: mockRequestText,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const mockSend = vi.fn();
|
|
||||||
vi.mock('~/store/callbackActions', () => ({
|
|
||||||
useCallbackActionsStore: () => ({
|
|
||||||
send: mockSend,
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const mockStableRelease: Partial<ServerUpdateOsResponse> = {
|
|
||||||
version: '6.12.5',
|
|
||||||
name: 'Unraid 6.12.5',
|
|
||||||
date: '2023-10-15',
|
|
||||||
isEligible: true,
|
|
||||||
isNewer: true,
|
|
||||||
changelog: 'https://example.com/changelog.md',
|
|
||||||
changelogPretty: 'https://example.com/changelog',
|
|
||||||
sha256: 'test-sha256',
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockBetaRelease: Partial<ServerUpdateOsResponse> = {
|
|
||||||
...mockStableRelease,
|
|
||||||
version: '6.12.5-beta1',
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('UpdateOsChangelog Store', () => {
|
|
||||||
let store: ReturnType<typeof useUpdateOsChangelogStore>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
setActivePinia(createPinia());
|
|
||||||
store = useUpdateOsChangelogStore();
|
|
||||||
vi.clearAllMocks();
|
|
||||||
|
|
||||||
// Suppress console output
|
|
||||||
vi.spyOn(console, 'debug').mockImplementation(() => {});
|
|
||||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Store API', () => {
|
|
||||||
it('should initialize with default values', () => {
|
|
||||||
expect(store.releaseForUpdate).toBeNull();
|
|
||||||
expect(store.parseChangelogFailed).toBe('');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set and get releaseForUpdate', () => {
|
|
||||||
store.setReleaseForUpdate(mockStableRelease as ServerUpdateOsResponse);
|
|
||||||
expect(store.releaseForUpdate).toEqual(mockStableRelease);
|
|
||||||
|
|
||||||
store.setReleaseForUpdate(null);
|
|
||||||
expect(store.releaseForUpdate).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should determine if release is stable', () => {
|
|
||||||
expect(store.isReleaseForUpdateStable).toBe(false);
|
|
||||||
|
|
||||||
store.setReleaseForUpdate(mockStableRelease as ServerUpdateOsResponse);
|
|
||||||
|
|
||||||
expect(store.isReleaseForUpdateStable).toBe(true);
|
|
||||||
|
|
||||||
store.setReleaseForUpdate(mockBetaRelease as ServerUpdateOsResponse);
|
|
||||||
expect(store.isReleaseForUpdateStable).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have a method to fetch and confirm install', () => {
|
|
||||||
store.fetchAndConfirmInstall('test-sha256');
|
|
||||||
|
|
||||||
expect(mockSend).toHaveBeenCalledWith(
|
|
||||||
expect.any(String),
|
|
||||||
[
|
|
||||||
{
|
|
||||||
sha256: 'test-sha256',
|
|
||||||
type: 'updateOs',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
undefined,
|
|
||||||
'forUpc'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have computed properties for changelog display', async () => {
|
|
||||||
store.setReleaseForUpdate(mockStableRelease as ServerUpdateOsResponse);
|
|
||||||
|
|
||||||
expect(typeof store.mutatedParsedChangelog).toBe('string');
|
|
||||||
expect(typeof store.parsedChangelogTitle).toBe('string');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should clear changelog data when release is set to null', () => {
|
|
||||||
store.setReleaseForUpdate(mockStableRelease as ServerUpdateOsResponse);
|
|
||||||
|
|
||||||
store.setReleaseForUpdate(null);
|
|
||||||
|
|
||||||
expect(store.releaseForUpdate).toBeNull();
|
|
||||||
expect(store.parseChangelogFailed).toBe('');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle state transitions when changing releases', () => {
|
|
||||||
store.setReleaseForUpdate(mockStableRelease as ServerUpdateOsResponse);
|
|
||||||
|
|
||||||
const differentRelease = {
|
|
||||||
...mockStableRelease,
|
|
||||||
version: '6.12.6',
|
|
||||||
};
|
|
||||||
store.setReleaseForUpdate(differentRelease as ServerUpdateOsResponse);
|
|
||||||
|
|
||||||
expect(store.releaseForUpdate).toEqual(differentRelease);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have proper error handling for failed requests', async () => {
|
|
||||||
mockRequestText.mockRejectedValueOnce(new Error('Network error'));
|
|
||||||
store.setReleaseForUpdate(mockStableRelease as ServerUpdateOsResponse);
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
expect(store.parseChangelogFailed).toBeTruthy();
|
|
||||||
expect(store.parseChangelogFailed).toContain('error');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fetch and parse changelog when releaseForUpdate changes', async () => {
|
|
||||||
const internalStore = useUpdateOsChangelogStore();
|
|
||||||
|
|
||||||
vi.clearAllMocks();
|
|
||||||
|
|
||||||
internalStore.setReleaseForUpdate(mockStableRelease as ServerUpdateOsResponse);
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
expect(mockRequestText).toHaveBeenCalled();
|
|
||||||
|
|
||||||
mockRequestText.mockClear();
|
|
||||||
|
|
||||||
const differentRelease = {
|
|
||||||
...mockStableRelease,
|
|
||||||
version: '6.12.6',
|
|
||||||
changelog: 'https://example.com/different-changelog.md',
|
|
||||||
};
|
|
||||||
|
|
||||||
internalStore.setReleaseForUpdate(differentRelease as ServerUpdateOsResponse);
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
expect(mockRequestText).toHaveBeenCalled();
|
|
||||||
|
|
||||||
mockRequestText.mockClear();
|
|
||||||
|
|
||||||
internalStore.setReleaseForUpdate(null);
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
expect(mockRequestText).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -47,7 +47,7 @@ import type {
|
|||||||
// EBLACKLISTED2
|
// EBLACKLISTED2
|
||||||
// ENOCONN
|
// ENOCONN
|
||||||
|
|
||||||
const state: ServerState = 'ENOKEYFILE' as ServerState;
|
const state: ServerState = 'BASIC' as ServerState;
|
||||||
const currentFlashGuid = '1111-1111-YIJD-ZACK1234TEST'; // this is the flash drive that's been booted from
|
const currentFlashGuid = '1111-1111-YIJD-ZACK1234TEST'; // this is the flash drive that's been booted from
|
||||||
const regGuid = '1111-1111-YIJD-ZACK1234TEST'; // this guid is registered in key server
|
const regGuid = '1111-1111-YIJD-ZACK1234TEST'; // this guid is registered in key server
|
||||||
const keyfileBase64 = '';
|
const keyfileBase64 = '';
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ const computedVerticalCenter = computed<string>(() => {
|
|||||||
]"
|
]"
|
||||||
class="text-16px text-foreground bg-background text-left relative z-10 mx-auto flex flex-col justify-around border-2 border-solid transform overflow-hidden rounded-lg transition-all"
|
class="text-16px text-foreground bg-background text-left relative z-10 mx-auto flex flex-col justify-around border-2 border-solid transform overflow-hidden rounded-lg transition-all"
|
||||||
>
|
>
|
||||||
<div v-if="showCloseX" class="absolute z-20 right-0 top-0 pt-4px pr-4px hidden sm:block">
|
<div v-if="showCloseX" class="absolute z-20 right-0 top-0 pt-4px pr-4px sm:block">
|
||||||
<button
|
<button
|
||||||
class="rounded-md text-foreground bg-transparent p-2 hover:text-white focus:text-white hover:bg-unraid-red focus:bg-unraid-red focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
class="rounded-md text-foreground bg-transparent p-2 hover:text-white focus:text-white hover:bg-unraid-red focus:bg-unraid-red focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -5,14 +5,12 @@ import { storeToRefs } from 'pinia';
|
|||||||
import { useCallbackActionsStore } from '~/store/callbackActions';
|
import { useCallbackActionsStore } from '~/store/callbackActions';
|
||||||
import { useTrialStore } from '~/store/trial';
|
import { useTrialStore } from '~/store/trial';
|
||||||
import { useUpdateOsStore } from '~/store/updateOs';
|
import { useUpdateOsStore } from '~/store/updateOs';
|
||||||
import { useUpdateOsChangelogStore } from '~/store/updateOsChangelog';
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const { callbackStatus } = storeToRefs(useCallbackActionsStore());
|
const { callbackStatus } = storeToRefs(useCallbackActionsStore());
|
||||||
const { trialModalVisible } = storeToRefs(useTrialStore());
|
const { trialModalVisible } = storeToRefs(useTrialStore());
|
||||||
const { modalOpen: updateOsModalVisible } = storeToRefs(useUpdateOsStore());
|
const { updateOsModalVisible, changelogModalVisible } = storeToRefs(useUpdateOsStore());
|
||||||
const { releaseForUpdate: updateOsChangelogModalVisible } = storeToRefs(useUpdateOsChangelogStore());
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -20,7 +18,7 @@ const { releaseForUpdate: updateOsChangelogModalVisible } = storeToRefs(useUpdat
|
|||||||
<UpcCallbackFeedback :t="t" :open="callbackStatus !== 'ready'" />
|
<UpcCallbackFeedback :t="t" :open="callbackStatus !== 'ready'" />
|
||||||
<UpcTrial :t="t" :open="trialModalVisible" />
|
<UpcTrial :t="t" :open="trialModalVisible" />
|
||||||
<UpdateOsCheckUpdateResponseModal :t="t" :open="updateOsModalVisible" />
|
<UpdateOsCheckUpdateResponseModal :t="t" :open="updateOsModalVisible" />
|
||||||
<UpdateOsChangelogModal :t="t" :open="!!updateOsChangelogModalVisible" />
|
<UpdateOsChangelogModal :t="t" :open="changelogModalVisible" />
|
||||||
<ActivationModal :t="t" />
|
<ActivationModal :t="t" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,23 +1,22 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ArrowSmallRightIcon,
|
ArrowLeftIcon,
|
||||||
|
ArrowRightIcon,
|
||||||
ArrowTopRightOnSquareIcon,
|
ArrowTopRightOnSquareIcon,
|
||||||
EyeIcon,
|
|
||||||
KeyIcon,
|
KeyIcon,
|
||||||
ServerStackIcon,
|
ServerStackIcon,
|
||||||
XMarkIcon,
|
|
||||||
} from '@heroicons/vue/24/solid';
|
} from '@heroicons/vue/24/solid';
|
||||||
import { BrandButton, BrandLoading } from '@unraid/ui';
|
import { BrandButton, BrandLoading } from '@unraid/ui';
|
||||||
|
|
||||||
import type { ComposerTranslation } from 'vue-i18n';
|
import type { ComposerTranslation } from 'vue-i18n';
|
||||||
|
|
||||||
|
import RawChangelogRenderer from '~/components/UpdateOs/RawChangelogRenderer.vue';
|
||||||
import { usePurchaseStore } from '~/store/purchase';
|
import { usePurchaseStore } from '~/store/purchase';
|
||||||
import { useUpdateOsStore } from '~/store/updateOs';
|
import { useUpdateOsStore } from '~/store/updateOs';
|
||||||
// import { useUpdateOsActionsStore } from '~/store/updateOsActions';
|
import { allowedDocsOriginRegex, allowedDocsUrlRegex } from '~/helpers/urls';
|
||||||
import { useUpdateOsChangelogStore } from '~/store/updateOsChangelog';
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
@@ -30,86 +29,143 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
|
|
||||||
const purchaseStore = usePurchaseStore();
|
const purchaseStore = usePurchaseStore();
|
||||||
const updateOsStore = useUpdateOsStore();
|
const updateOsStore = useUpdateOsStore();
|
||||||
// const updateOsActionsStore = useUpdateOsActionsStore();
|
const { availableWithRenewal, releaseForUpdate, changelogModalVisible } = storeToRefs(updateOsStore);
|
||||||
const updateOsChangelogStore = useUpdateOsChangelogStore();
|
const { setReleaseForUpdate, fetchAndConfirmInstall } = updateOsStore;
|
||||||
|
|
||||||
const { availableWithRenewal } = storeToRefs(updateOsStore);
|
|
||||||
const { releaseForUpdate, mutatedParsedChangelog, parseChangelogFailed, parsedChangelogTitle } =
|
|
||||||
storeToRefs(updateOsChangelogStore);
|
|
||||||
|
|
||||||
const showExtendKeyButton = computed(() => {
|
const showExtendKeyButton = computed(() => {
|
||||||
return availableWithRenewal.value;
|
return availableWithRenewal.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// iframe navigation handling
|
||||||
|
const iframeRef = ref<HTMLIFrameElement | null>(null);
|
||||||
|
const hasNavigated = ref(false);
|
||||||
|
const currentIframeUrl = ref<string | null>(null);
|
||||||
|
|
||||||
|
const docsChangelogUrl = computed(() => {
|
||||||
|
return releaseForUpdate.value?.changelogPretty ?? null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const showRawChangelog = computed<boolean>(() => {
|
||||||
|
return !docsChangelogUrl.value && !!releaseForUpdate.value?.changelog;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleIframeNavigationMessage = (event: MessageEvent) => {
|
||||||
|
if (
|
||||||
|
event.data &&
|
||||||
|
event.data.type === 'unraid-docs-navigation' &&
|
||||||
|
iframeRef.value &&
|
||||||
|
event.source === iframeRef.value.contentWindow &&
|
||||||
|
allowedDocsOriginRegex.test(event.origin)
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
typeof event.data.url === 'string' &&
|
||||||
|
allowedDocsUrlRegex.test(event.data.url)
|
||||||
|
) {
|
||||||
|
if (event.data.url !== docsChangelogUrl.value) {
|
||||||
|
hasNavigated.value = true;
|
||||||
|
} else {
|
||||||
|
hasNavigated.value = false;
|
||||||
|
}
|
||||||
|
currentIframeUrl.value = event.data.url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('message', handleIframeNavigationMessage);
|
||||||
|
// Set initial value
|
||||||
|
currentIframeUrl.value = docsChangelogUrl.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('message', handleIframeNavigationMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
const revertToInitialChangelog = () => {
|
||||||
|
if (iframeRef.value && docsChangelogUrl.value) {
|
||||||
|
iframeRef.value.src = docsChangelogUrl.value;
|
||||||
|
hasNavigated.value = false;
|
||||||
|
currentIframeUrl.value = docsChangelogUrl.value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(docsChangelogUrl, (newUrl) => {
|
||||||
|
currentIframeUrl.value = newUrl;
|
||||||
|
hasNavigated.value = false;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Modal
|
<Modal
|
||||||
|
v-if="releaseForUpdate?.version"
|
||||||
:center-content="false"
|
:center-content="false"
|
||||||
:error="!!parseChangelogFailed"
|
|
||||||
max-width="max-w-800px"
|
max-width="max-w-800px"
|
||||||
:open="!!releaseForUpdate"
|
:open="changelogModalVisible"
|
||||||
:show-close-x="true"
|
:show-close-x="true"
|
||||||
:t="t"
|
:t="t"
|
||||||
:tall-content="true"
|
:tall-content="true"
|
||||||
:title="parsedChangelogTitle ?? undefined"
|
:title="t('Unraid OS {0} Changelog', [releaseForUpdate.version])"
|
||||||
@close="updateOsChangelogStore.setReleaseForUpdate(null)"
|
:disable-overlay-close="false"
|
||||||
|
@close="setReleaseForUpdate(null)"
|
||||||
>
|
>
|
||||||
<template #main>
|
<template #main>
|
||||||
<div
|
<div class="flex flex-col gap-4 min-w-[280px] sm:min-w-[400px]">
|
||||||
v-if="mutatedParsedChangelog"
|
<!-- iframe for changelog if available -->
|
||||||
class="text-16px sm:text-18px prose prose-a:text-unraid-red hover:prose-a:no-underline hover:prose-a:text-unraid-red/60 dark:prose-a:text-orange hover:dark:prose-a:text-orange/60"
|
<div v-if="docsChangelogUrl" class="w-[calc(100%+3rem)] h-[475px] -mx-6 -my-6">
|
||||||
v-html="mutatedParsedChangelog"
|
<iframe
|
||||||
/>
|
ref="iframeRef"
|
||||||
|
:src="docsChangelogUrl"
|
||||||
<div v-else-if="parseChangelogFailed" class="text-center flex flex-col gap-4 prose">
|
class="w-full h-full border-0 rounded-md"
|
||||||
<h2 class="text-lg text-unraid-red italic font-semibold">
|
sandbox="allow-scripts allow-same-origin"
|
||||||
{{ props.t(`Error Parsing Changelog • {0}`, [parseChangelogFailed]) }}
|
title="Unraid Changelog"
|
||||||
</h2>
|
></iframe>
|
||||||
<p>
|
|
||||||
{{ props.t(`It's highly recommended to review the changelog before continuing your update`) }}
|
|
||||||
</p>
|
|
||||||
<div v-if="releaseForUpdate?.changelogPretty" class="flex self-center">
|
|
||||||
<BrandButton
|
|
||||||
:href="releaseForUpdate?.changelogPretty"
|
|
||||||
variant="underline"
|
|
||||||
:external="true"
|
|
||||||
:icon-right="ArrowTopRightOnSquareIcon"
|
|
||||||
>
|
|
||||||
{{ props.t('View Changelog on Docs') }}
|
|
||||||
</BrandButton>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<!-- Fallback to raw changelog -->
|
||||||
v-else
|
<RawChangelogRenderer
|
||||||
class="text-center flex flex-col justify-center w-full min-h-[250px] min-w-[280px] sm:min-w-[400px]"
|
v-else-if="showRawChangelog && releaseForUpdate?.changelog"
|
||||||
>
|
:changelog="releaseForUpdate?.changelog"
|
||||||
<BrandLoading class="w-[150px] mx-auto mt-24px" />
|
:version="releaseForUpdate?.version"
|
||||||
<p>{{ props.t('Fetching & parsing changelog…') }}</p>
|
:date="releaseForUpdate?.date"
|
||||||
|
:t="t"
|
||||||
|
:changelog-pretty="releaseForUpdate?.changelogPretty"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Loading state -->
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="text-center flex flex-col justify-center w-full min-h-[250px] min-w-[280px] sm:min-w-[400px]"
|
||||||
|
>
|
||||||
|
<BrandLoading class="w-[150px] mx-auto mt-24px" />
|
||||||
|
<p>{{ props.t('Loading changelog…') }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex flex-col-reverse xs:flex-row justify-between gap-12px md:gap-16px">
|
<div class="flex flex-col-reverse xs:flex-row justify-between gap-12px md:gap-16px">
|
||||||
<div class="flex flex-col-reverse xs:flex-row xs:justify-start gap-12px md:gap-16px">
|
<div class="flex flex-col-reverse xs:flex-row xs:justify-start gap-12px md:gap-16px">
|
||||||
|
<!-- Back to changelog button (when navigated away) -->
|
||||||
<BrandButton
|
<BrandButton
|
||||||
|
v-if="hasNavigated && docsChangelogUrl"
|
||||||
variant="underline"
|
variant="underline"
|
||||||
:icon="XMarkIcon"
|
:icon="ArrowLeftIcon"
|
||||||
@click="updateOsChangelogStore.setReleaseForUpdate(null)"
|
aria-label="Back to Changelog"
|
||||||
>
|
@click="revertToInitialChangelog"
|
||||||
{{ props.t('Close') }}
|
/>
|
||||||
</BrandButton>
|
|
||||||
|
<!-- View on docs button -->
|
||||||
<BrandButton
|
<BrandButton
|
||||||
v-if="releaseForUpdate?.changelogPretty"
|
v-if="currentIframeUrl || releaseForUpdate?.changelogPretty"
|
||||||
variant="underline"
|
variant="underline"
|
||||||
:external="true"
|
:external="true"
|
||||||
:href="releaseForUpdate?.changelogPretty"
|
:href="currentIframeUrl || releaseForUpdate?.changelogPretty"
|
||||||
:icon="EyeIcon"
|
:icon="ArrowTopRightOnSquareIcon"
|
||||||
:icon-right="ArrowTopRightOnSquareIcon"
|
aria-label="View on Docs"
|
||||||
>
|
/>
|
||||||
{{ props.t('View on Docs') }}
|
|
||||||
</BrandButton>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Action buttons -->
|
||||||
<BrandButton
|
<BrandButton
|
||||||
v-if="showExtendKeyButton"
|
v-if="showExtendKeyButton"
|
||||||
variant="fill"
|
variant="fill"
|
||||||
@@ -122,8 +178,8 @@ const showExtendKeyButton = computed(() => {
|
|||||||
<BrandButton
|
<BrandButton
|
||||||
v-else-if="releaseForUpdate?.sha256"
|
v-else-if="releaseForUpdate?.sha256"
|
||||||
:icon="ServerStackIcon"
|
:icon="ServerStackIcon"
|
||||||
:icon-right="ArrowSmallRightIcon"
|
:icon-right="ArrowRightIcon"
|
||||||
@click="updateOsChangelogStore.fetchAndConfirmInstall(releaseForUpdate.sha256)"
|
@click="fetchAndConfirmInstall(releaseForUpdate.sha256)"
|
||||||
>
|
>
|
||||||
{{ props.t('Continue') }}
|
{{ props.t('Continue') }}
|
||||||
</BrandButton>
|
</BrandButton>
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import { useAccountStore } from '~/store/account';
|
|||||||
import { usePurchaseStore } from '~/store/purchase';
|
import { usePurchaseStore } from '~/store/purchase';
|
||||||
import { useServerStore } from '~/store/server';
|
import { useServerStore } from '~/store/server';
|
||||||
import { useUpdateOsStore } from '~/store/updateOs';
|
import { useUpdateOsStore } from '~/store/updateOs';
|
||||||
import { useUpdateOsChangelogStore } from '~/store/updateOsChangelog';
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
@@ -35,7 +34,6 @@ const accountStore = useAccountStore();
|
|||||||
const purchaseStore = usePurchaseStore();
|
const purchaseStore = usePurchaseStore();
|
||||||
const serverStore = useServerStore();
|
const serverStore = useServerStore();
|
||||||
const updateOsStore = useUpdateOsStore();
|
const updateOsStore = useUpdateOsStore();
|
||||||
const updateOsChangelogStore = useUpdateOsChangelogStore();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
regExp,
|
regExp,
|
||||||
@@ -181,7 +179,7 @@ const actionButtons = computed((): BrandButtonProps[] | null => {
|
|||||||
buttons.push({
|
buttons.push({
|
||||||
variant: availableWithRenewal.value ? 'outline' : undefined,
|
variant: availableWithRenewal.value ? 'outline' : undefined,
|
||||||
click: async () =>
|
click: async () =>
|
||||||
await updateOsChangelogStore.setReleaseForUpdate(updateOsResponse.value ?? null),
|
await updateOsStore.setReleaseForUpdate(updateOsResponse.value ?? null),
|
||||||
icon: EyeIcon,
|
icon: EyeIcon,
|
||||||
text: availableWithRenewal.value
|
text: availableWithRenewal.value
|
||||||
? props.t('View Changelog')
|
? props.t('View Changelog')
|
||||||
|
|||||||
141
web/components/UpdateOs/RawChangelogRenderer.vue
Normal file
141
web/components/UpdateOs/RawChangelogRenderer.vue
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, onMounted, watch } from 'vue';
|
||||||
|
import { Markdown } from '@/helpers/markdown';
|
||||||
|
import { BrandButton } from '@unraid/ui';
|
||||||
|
import { ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid';
|
||||||
|
import type { ComposerTranslation } from 'vue-i18n';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
changelog: string; // This is the URL to the changelog, not the content
|
||||||
|
version?: string;
|
||||||
|
date?: string;
|
||||||
|
t: ComposerTranslation;
|
||||||
|
changelogPretty?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const parsedChangelog = ref<string>("");
|
||||||
|
const parsedChangelogTitle = ref<string>("");
|
||||||
|
const parseChangelogFailed = ref<string>("");
|
||||||
|
const isLoading = ref<boolean>(false);
|
||||||
|
|
||||||
|
// Fetch and parse the changelog using the same logic as in updateOsChangelog.ts
|
||||||
|
const fetchAndParseChangelog = async () => {
|
||||||
|
if (!props.changelog) {
|
||||||
|
isLoading.value = false;
|
||||||
|
parseChangelogFailed.value = 'No changelog URL provided';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isLoading.value = true;
|
||||||
|
parseChangelogFailed.value = "";
|
||||||
|
try {
|
||||||
|
// Fetch the changelog content from the URL
|
||||||
|
const res = await fetch(props.changelog);
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`HTTP ${res.status} while fetching changelog`);
|
||||||
|
}
|
||||||
|
const data = await res.text();
|
||||||
|
if (!data || data.trim() === "") {
|
||||||
|
parseChangelogFailed.value = 'Changelog is empty';
|
||||||
|
isLoading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let firstHeading = true;
|
||||||
|
const marked = Markdown.create();
|
||||||
|
// open links in new tab & replace .md from links
|
||||||
|
const renderer = new marked.Renderer();
|
||||||
|
// Set base URL for relative links
|
||||||
|
const baseUrl = "https://docs.unraid.net/go/release-notes/";
|
||||||
|
renderer.link = ({ href, title, tokens }) => {
|
||||||
|
const linkText = renderer.parser.parseInline(tokens);
|
||||||
|
let cleanHref = href.replace(".md", ""); // remove .md from href
|
||||||
|
if (cleanHref.startsWith("#")) {
|
||||||
|
cleanHref = `${baseUrl}${props.version}${cleanHref}`;
|
||||||
|
}
|
||||||
|
if (!cleanHref.startsWith("http")) {
|
||||||
|
cleanHref = `${baseUrl}${cleanHref}`;
|
||||||
|
}
|
||||||
|
return `<a href="${cleanHref}" ${title ? `title="${title}"` : ""} target="_blank" rel="noopener noreferrer">${linkText}</a>`;
|
||||||
|
};
|
||||||
|
// Add heading renderer
|
||||||
|
renderer.heading = ({ text, depth }) => {
|
||||||
|
// Capture the first h1 title
|
||||||
|
if (depth === 1 && firstHeading) {
|
||||||
|
firstHeading = false;
|
||||||
|
parsedChangelogTitle.value = `Version ${props.version} ${props.date ? `(${props.date})` : ''}`;
|
||||||
|
return `<br />`;
|
||||||
|
}
|
||||||
|
// Only add IDs for h2 and above (depth > 1)
|
||||||
|
if (depth > 1) {
|
||||||
|
const id = text
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^\w\s-]/g, "")
|
||||||
|
.replace(/\s+/g, "-");
|
||||||
|
return `<h${depth} id="${id}">${text}</h${depth}>`;
|
||||||
|
}
|
||||||
|
return `<h${depth}>${text}</h${depth}>`;
|
||||||
|
};
|
||||||
|
marked.setOptions({
|
||||||
|
renderer,
|
||||||
|
});
|
||||||
|
parsedChangelog.value = await marked.parse(
|
||||||
|
(data as string) ?? "Changelog fetch data empty"
|
||||||
|
);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const caughtError = error as Error;
|
||||||
|
parseChangelogFailed.value =
|
||||||
|
caughtError && caughtError?.message
|
||||||
|
? caughtError.message
|
||||||
|
: `Failed to parse ${props.version} changelog`;
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse the changelog when the component is mounted or props change
|
||||||
|
onMounted(fetchAndParseChangelog);
|
||||||
|
watch(() => props.changelog, fetchAndParseChangelog, { immediate: true });
|
||||||
|
|
||||||
|
// Used to remove the first <h1></h1> and its contents from the parsedChangelog
|
||||||
|
const mutatedParsedChangelog = computed(() => {
|
||||||
|
if (parsedChangelog.value) {
|
||||||
|
return parsedChangelog.value.replace(/<h1>(.*?)<\/h1>/, "");
|
||||||
|
}
|
||||||
|
return parsedChangelog.value;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="prose prose-sm dark:prose-invert max-w-none markdown-body p-4 overflow-auto">
|
||||||
|
<div v-if="parseChangelogFailed" class="text-center flex flex-col gap-4 prose">
|
||||||
|
<h2 class="text-lg text-unraid-red italic font-semibold">
|
||||||
|
{{ props.t(`Error Parsing Changelog • {0}`, [parseChangelogFailed]) }}
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
{{
|
||||||
|
props.t(`It's highly recommended to review the changelog before continuing your update`)
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
<div v-if="props.changelogPretty" class="flex self-center">
|
||||||
|
<BrandButton
|
||||||
|
:href="props.changelogPretty"
|
||||||
|
variant="underline"
|
||||||
|
:external="true"
|
||||||
|
:icon-right="ArrowTopRightOnSquareIcon"
|
||||||
|
>
|
||||||
|
{{ props.t('View Changelog on Docs') }}
|
||||||
|
</BrandButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="parsedChangelogTitle" class="mb-4">
|
||||||
|
<h1>{{ parsedChangelogTitle }}</h1>
|
||||||
|
</div>
|
||||||
|
<div v-if="mutatedParsedChangelog" v-html="mutatedParsedChangelog"></div>
|
||||||
|
<div v-else-if="isLoading" class="flex flex-col items-center justify-center py-8">
|
||||||
|
<span class="mx-auto animate-spin border-2 border-gray-300 rounded-full w-8 h-8 border-t-unraid-red"></span>
|
||||||
|
<p class="ml-2">{{ props.t('Loading changelog...') }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-center py-8">
|
||||||
|
<p>{{ props.t('No changelog content available') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -40,6 +40,9 @@ const DOCS_REGISTRATION_REPLACE_KEY = new URL('/go/changing-the-flash-device/',
|
|||||||
|
|
||||||
const SUPPORT = new URL('https://unraid.net');
|
const SUPPORT = new URL('https://unraid.net');
|
||||||
|
|
||||||
|
export const allowedDocsOriginRegex = /^https:\/\/(?:[\w-]+\.)*docs\.unraid\.net(?::\d+)?$/;
|
||||||
|
export const allowedDocsUrlRegex = /^https:\/\/(?:[\w-]+\.)*docs\.unraid\.net(?::\d+)?\//;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ACCOUNT,
|
ACCOUNT,
|
||||||
ACCOUNT_CALLBACK,
|
ACCOUNT_CALLBACK,
|
||||||
|
|||||||
@@ -384,5 +384,6 @@
|
|||||||
"Your Trial key has been extended!": "Your Trial key has been extended!",
|
"Your Trial key has been extended!": "Your Trial key has been extended!",
|
||||||
"Create an Unraid.net account and activate your key": "Create an Unraid.net account and activate your key",
|
"Create an Unraid.net account and activate your key": "Create an Unraid.net account and activate your key",
|
||||||
"Device is ready to configure": "Device is ready to configure",
|
"Device is ready to configure": "Device is ready to configure",
|
||||||
"Secure your device": "Secure your device"
|
"Secure your device": "Secure your device",
|
||||||
|
"Unraid OS {0} Changelog": "Unraid OS {0} Changelog"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,88 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useUpdateOsChangelogStore } from '~/store/updateOsChangelog';
|
import { useUpdateOsStore } from '~/store/updateOs';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
const changelog = ref('');
|
const updateOsStore = useUpdateOsStore();
|
||||||
const updateOsChangelogStore = useUpdateOsChangelogStore();
|
const { changelogModalVisible } = storeToRefs(updateOsStore);
|
||||||
const { releaseForUpdate: updateOsChangelogModalVisible } = storeToRefs(updateOsChangelogStore);
|
const { t } = useI18n();
|
||||||
onMounted(async () => {
|
|
||||||
const response = await fetch('https://releases.unraid.net/json');
|
|
||||||
|
async function showChangelogModalFromReleasesEndpoint() {
|
||||||
|
const response = await fetch('https://releases.unraid.net/os?branch=stable¤t_version=6.12.3');
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.length > 0) {
|
updateOsStore.setReleaseForUpdate(data);
|
||||||
changelog.value = data[0].changelog;
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function showChangelogModal() {
|
function showChangelogModalWithTestData() {
|
||||||
// Simulate receiving a release for update
|
updateOsStore.setReleaseForUpdate({
|
||||||
updateOsChangelogStore.setReleaseForUpdate({
|
|
||||||
version: '6.12.3',
|
version: '6.12.3',
|
||||||
date: '2023-07-15',
|
date: '2023-07-15',
|
||||||
changelog: changelog.value,
|
changelog: 'https://raw.githubusercontent.com/unraid/docs/main/docs/unraid-os/release-notes/6.12.3.md',
|
||||||
|
changelogPretty: 'https://docs.unraid.net/go/release-notes/6.12.3',
|
||||||
name: '6.12.3',
|
name: '6.12.3',
|
||||||
isEligible: true,
|
isEligible: true,
|
||||||
isNewer: true,
|
isNewer: true,
|
||||||
|
sha256: '1234567890'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function showChangelogWithoutPretty() {
|
||||||
|
updateOsStore.setReleaseForUpdate({
|
||||||
|
version: '6.12.3',
|
||||||
|
date: '2023-07-15',
|
||||||
|
changelog: 'https://raw.githubusercontent.com/unraid/docs/main/docs/unraid-os/release-notes/6.12.3.md',
|
||||||
|
changelogPretty: '',
|
||||||
|
name: '6.12.3',
|
||||||
|
isEligible: true,
|
||||||
|
isNewer: true,
|
||||||
|
sha256: '1234567890'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showChangelogBrokenParse() {
|
||||||
|
updateOsStore.setReleaseForUpdate({
|
||||||
|
version: '6.12.3',
|
||||||
|
date: '2023-07-15',
|
||||||
|
changelog: null,
|
||||||
|
changelogPretty: undefined, // intentionally broken
|
||||||
|
name: '6.12.3',
|
||||||
|
isEligible: true,
|
||||||
|
isNewer: true,
|
||||||
sha256: '1234567890'
|
sha256: '1234567890'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="container mx-auto p-6">
|
<div class="container mx-auto p-6">
|
||||||
<h1 class="text-2xl font-bold mb-6">Changelog</h1>
|
<h1 class="text-2xl font-bold mb-6">Changelog</h1>
|
||||||
<UpdateOsChangelogModal :t="t" :open="!!updateOsChangelogModalVisible" />
|
<UpdateOsChangelogModal :t="t" :open="changelogModalVisible" />
|
||||||
<div class="mb-6">
|
<div class="mb-6 flex flex-col gap-4 max-w-md">
|
||||||
<button
|
<button
|
||||||
class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||||
@click="showChangelogModal"
|
@click="showChangelogModalFromReleasesEndpoint"
|
||||||
>
|
>
|
||||||
Test Changelog Modal
|
Test Changelog Modal (from releases endpoint)
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
|
||||||
|
@click="showChangelogModalWithTestData"
|
||||||
|
>
|
||||||
|
Test Changelog Modal (with test data)
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"
|
||||||
|
@click="showChangelogWithoutPretty"
|
||||||
|
>
|
||||||
|
Test Without Pretty Changelog
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="px-4 py-2 bg-yellow-500 text-white rounded hover:bg-yellow-600"
|
||||||
|
@click="showChangelogBrokenParse"
|
||||||
|
>
|
||||||
|
Test Broken Parse Changelog
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="prose max-w-none">
|
|
||||||
<div v-html="changelog"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import type { ServerUpdateOsResponse } from '~/types/server';
|
|||||||
|
|
||||||
import { WebguiCheckForUpdate, WebguiUpdateCancel } from '~/composables/services/webgui';
|
import { WebguiCheckForUpdate, WebguiUpdateCancel } from '~/composables/services/webgui';
|
||||||
import { useServerStore } from '~/store/server';
|
import { useServerStore } from '~/store/server';
|
||||||
|
import prerelease from 'semver/functions/prerelease';
|
||||||
|
import { useCallbackActionsStore } from '~/store/callbackActions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
|
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
|
||||||
@@ -23,7 +25,9 @@ extend(relativeTime);
|
|||||||
export const useUpdateOsStore = defineStore('updateOs', () => {
|
export const useUpdateOsStore = defineStore('updateOs', () => {
|
||||||
// state
|
// state
|
||||||
const checkForUpdatesLoading = ref<boolean>(false);
|
const checkForUpdatesLoading = ref<boolean>(false);
|
||||||
const modalOpen = ref<boolean>(false);
|
const updateOsModalVisible = ref<boolean>(false);
|
||||||
|
const releaseForUpdate = ref<ServerUpdateOsResponse | null>(null);
|
||||||
|
const changelogModalVisible = computed(() => !!releaseForUpdate.value);
|
||||||
// getters from other stores
|
// getters from other stores
|
||||||
const serverStore = useServerStore();
|
const serverStore = useServerStore();
|
||||||
|
|
||||||
@@ -59,6 +63,33 @@ export const useUpdateOsStore = defineStore('updateOs', () => {
|
|||||||
*/
|
*/
|
||||||
const availableRequiresAuth = computed((): boolean => !updateOsResponse.value?.sha256);
|
const availableRequiresAuth = computed((): boolean => !updateOsResponse.value?.sha256);
|
||||||
|
|
||||||
|
// Changelog logic
|
||||||
|
const changelogUrl = computed((): string => releaseForUpdate.value?.changelog || '');
|
||||||
|
const changelogPretty = computed(() => releaseForUpdate.value?.changelogPretty ?? null);
|
||||||
|
const setReleaseForUpdate = (release: ServerUpdateOsResponse | null) => {
|
||||||
|
releaseForUpdate.value = release;
|
||||||
|
};
|
||||||
|
// isReleaseForUpdateStable logic (true if no prerelease in version)
|
||||||
|
const isReleaseForUpdateStable = computed(() => {
|
||||||
|
if (!releaseForUpdate.value?.version) return false;
|
||||||
|
return !prerelease(releaseForUpdate.value.version);
|
||||||
|
});
|
||||||
|
// fetchAndConfirmInstall logic
|
||||||
|
const callbackStore = useCallbackActionsStore();
|
||||||
|
const fetchAndConfirmInstall = (sha256: string) => {
|
||||||
|
callbackStore.send(
|
||||||
|
window.location.href,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
sha256,
|
||||||
|
type: 'updateOs',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
undefined,
|
||||||
|
'forUpc'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// actions
|
// actions
|
||||||
const localCheckForUpdate = async (): Promise<void> => {
|
const localCheckForUpdate = async (): Promise<void> => {
|
||||||
checkForUpdatesLoading.value = true;
|
checkForUpdatesLoading.value = true;
|
||||||
@@ -95,7 +126,7 @@ export const useUpdateOsStore = defineStore('updateOs', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const setModalOpen = (val: boolean) => {
|
const setModalOpen = (val: boolean) => {
|
||||||
modalOpen.value = val;
|
updateOsModalVisible.value = val;
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -103,14 +134,21 @@ export const useUpdateOsStore = defineStore('updateOs', () => {
|
|||||||
available,
|
available,
|
||||||
availableWithRenewal,
|
availableWithRenewal,
|
||||||
checkForUpdatesLoading,
|
checkForUpdatesLoading,
|
||||||
modalOpen,
|
updateOsModalVisible,
|
||||||
|
changelogModalVisible,
|
||||||
|
releaseForUpdate,
|
||||||
updateOsIgnoredReleases,
|
updateOsIgnoredReleases,
|
||||||
// getters
|
// getters
|
||||||
availableReleaseDate,
|
availableReleaseDate,
|
||||||
availableRequiresAuth,
|
availableRequiresAuth,
|
||||||
|
changelogUrl,
|
||||||
|
changelogPretty,
|
||||||
|
isReleaseForUpdateStable,
|
||||||
// actions
|
// actions
|
||||||
localCheckForUpdate,
|
localCheckForUpdate,
|
||||||
cancelUpdate,
|
cancelUpdate,
|
||||||
setModalOpen,
|
setModalOpen,
|
||||||
|
setReleaseForUpdate,
|
||||||
|
fetchAndConfirmInstall,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,132 +0,0 @@
|
|||||||
import { computed, ref, watch } from 'vue';
|
|
||||||
import { defineStore } from 'pinia';
|
|
||||||
|
|
||||||
import { Markdown } from '@/helpers/markdown';
|
|
||||||
import { DOCS_RELEASE_NOTES } from '~/helpers/urls';
|
|
||||||
import { baseUrl } from 'marked-base-url';
|
|
||||||
import prerelease from 'semver/functions/prerelease';
|
|
||||||
|
|
||||||
import type { ServerUpdateOsResponse } from '~/types/server';
|
|
||||||
|
|
||||||
import { request } from '~/composables/services/request';
|
|
||||||
import { useCallbackActionsStore } from '~/store/callbackActions';
|
|
||||||
|
|
||||||
export const useUpdateOsChangelogStore = defineStore('updateOsChangelog', () => {
|
|
||||||
const callbackStore = useCallbackActionsStore();
|
|
||||||
|
|
||||||
const releaseForUpdate = ref<ServerUpdateOsResponse | null>(null);
|
|
||||||
watch(releaseForUpdate, async (newVal, oldVal) => {
|
|
||||||
console.debug('[releaseForUpdate] watch', newVal, oldVal);
|
|
||||||
resetChangelogDetails(); // reset values when setting and unsetting a selected release
|
|
||||||
// Fetch and parse the changelog when the user selects a release
|
|
||||||
if (newVal) {
|
|
||||||
await fetchAndParseChangelog();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const changelogUrl = computed((): string => {
|
|
||||||
if (!releaseForUpdate.value || !releaseForUpdate.value?.changelog) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
releaseForUpdate.value?.changelog ??
|
|
||||||
`https://raw.githubusercontent.com/unraid/docs/main/docs/unraid-os/release-notes/${releaseForUpdate.value.version}.md`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const isReleaseForUpdateStable = computed(() =>
|
|
||||||
releaseForUpdate.value ? prerelease(releaseForUpdate.value.version) === null : false
|
|
||||||
);
|
|
||||||
const parsedChangelog = ref<string>('');
|
|
||||||
const parseChangelogFailed = ref<string>('');
|
|
||||||
// used to remove the first <h1></h1> and it's contents from the parsedChangelog
|
|
||||||
const mutatedParsedChangelog = computed(() => {
|
|
||||||
if (parsedChangelog.value) {
|
|
||||||
return parsedChangelog.value.replace(/<h1>(.*?)<\/h1>/, '');
|
|
||||||
}
|
|
||||||
return parsedChangelog.value;
|
|
||||||
});
|
|
||||||
// used to extract the first <h1></h1> and it's contents from the parsedChangelog for the modal header title
|
|
||||||
const parsedChangelogTitle = computed(() => {
|
|
||||||
if (parseChangelogFailed.value) {
|
|
||||||
return parseChangelogFailed.value;
|
|
||||||
}
|
|
||||||
if (parsedChangelog.value) {
|
|
||||||
return (
|
|
||||||
parsedChangelog.value.match(/<h1>(.*?)<\/h1>/)?.[1] ??
|
|
||||||
`Version ${releaseForUpdate.value?.version} ${releaseForUpdate.value?.date}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
});
|
|
||||||
|
|
||||||
const setReleaseForUpdate = (release: ServerUpdateOsResponse | null) => {
|
|
||||||
console.debug('[setReleaseForUpdate]', release);
|
|
||||||
releaseForUpdate.value = release;
|
|
||||||
};
|
|
||||||
const resetChangelogDetails = () => {
|
|
||||||
console.debug('[resetChangelogDetails]');
|
|
||||||
parsedChangelog.value = '';
|
|
||||||
parseChangelogFailed.value = '';
|
|
||||||
};
|
|
||||||
const fetchAndParseChangelog = async () => {
|
|
||||||
console.debug('[fetchAndParseChangelog]');
|
|
||||||
try {
|
|
||||||
const changelogMarkdownRaw = await request
|
|
||||||
.url(changelogUrl.value ?? '')
|
|
||||||
.get()
|
|
||||||
.text();
|
|
||||||
|
|
||||||
// set base url for relative links
|
|
||||||
const marked = Markdown.create(baseUrl(DOCS_RELEASE_NOTES.toString()));
|
|
||||||
|
|
||||||
// open links in new tab & replace .md from links
|
|
||||||
const renderer = new marked.Renderer();
|
|
||||||
|
|
||||||
renderer.link = ({ href, title, tokens }) => {
|
|
||||||
const linkText = renderer.parser.parseInline(tokens);
|
|
||||||
const cleanHref = href.replace('.md', ''); // remove .md from href
|
|
||||||
return `<a href="${cleanHref}" ${title ? `title="${title}"` : ''} target="_blank">${linkText}</a>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
marked.setOptions({
|
|
||||||
renderer,
|
|
||||||
});
|
|
||||||
|
|
||||||
parsedChangelog.value = await marked.parse(changelogMarkdownRaw);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
const caughtError = error as Error;
|
|
||||||
parseChangelogFailed.value =
|
|
||||||
caughtError && caughtError?.message
|
|
||||||
? caughtError.message
|
|
||||||
: `Failed to parse ${releaseForUpdate.value?.version} changelog`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchAndConfirmInstall = (sha256: string) => {
|
|
||||||
callbackStore.send(
|
|
||||||
window.location.href,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
sha256,
|
|
||||||
type: 'updateOs',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
undefined,
|
|
||||||
'forUpc'
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
// state
|
|
||||||
parseChangelogFailed,
|
|
||||||
releaseForUpdate,
|
|
||||||
// getters
|
|
||||||
isReleaseForUpdateStable,
|
|
||||||
mutatedParsedChangelog,
|
|
||||||
parsedChangelogTitle,
|
|
||||||
// actions
|
|
||||||
setReleaseForUpdate,
|
|
||||||
fetchAndConfirmInstall,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user