From fcd6fbcdd48e7f224b3bd8799a668d9e01967f0c Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Mon, 19 May 2025 12:34:44 -0700 Subject: [PATCH] feat: move to iframe for changelog (#1388) ## 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. --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- plugin/plugins/dynamix.unraid.net.plg | 9 +- .../include/translations.php | 1 + web/__test__/store/updateOs.test.ts | 8 +- web/__test__/store/updateOsChangelog.test.ts | 197 ------------------ web/_data/serverState.ts | 2 +- web/components/Modal.vue | 2 +- web/components/Modals.ce.vue | 6 +- web/components/UpdateOs/ChangelogModal.vue | 174 ++++++++++------ .../UpdateOs/CheckUpdateResponseModal.vue | 4 +- .../UpdateOs/RawChangelogRenderer.vue | 141 +++++++++++++ web/helpers/urls.ts | 3 + web/locales/en_US.json | 3 +- web/pages/changelog.vue | 88 +++++--- web/store/updateOs.ts | 44 +++- web/store/updateOsChangelog.ts | 132 ------------ 15 files changed, 380 insertions(+), 434 deletions(-) delete mode 100644 web/__test__/store/updateOsChangelog.test.ts create mode 100644 web/components/UpdateOs/RawChangelogRenderer.vue delete mode 100644 web/store/updateOsChangelog.ts diff --git a/plugin/plugins/dynamix.unraid.net.plg b/plugin/plugins/dynamix.unraid.net.plg index cf3ce5fad..38c4869a9 100755 --- a/plugin/plugins/dynamix.unraid.net.plg +++ b/plugin/plugins/dynamix.unraid.net.plg @@ -134,8 +134,8 @@ done # Handle the unraid-components directory DIR=/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components if [ -d "$DIR" ] && [ ! -d "$DIR-" ]; then - cp -rp "$DIR" "$DIR-" - echo "Backed up directory: $DIR" + mv "$DIR" "$DIR-" + echo "Moved directory: $DIR to $DIR-" fi echo "Backup complete." @@ -204,12 +204,13 @@ exit 0 # Handle the unraid-components directory 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 rm -rf "$DIR" fi if [ -d "$DIR-" ]; then - mv -f "$DIR-" "$DIR" + mv "$DIR-" "$DIR" + echo "Restored directory: $DIR- to $DIR" fi ]]> diff --git a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/translations.php b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/translations.php index c771872eb..2e28d4a55 100644 --- a/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/translations.php +++ b/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/translations.php @@ -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 Trial has expired' => _('Your Trial has expired'), 'Your Trial key has been extended!' => _('Your Trial key has been extended!'), + 'Unraid OS {0} Changelog' => sprintf(_('Unraid OS %s Changelog'), '{0}'), ]; } diff --git a/web/__test__/store/updateOs.test.ts b/web/__test__/store/updateOs.test.ts index 56ce2033c..c9c76e173 100644 --- a/web/__test__/store/updateOs.test.ts +++ b/web/__test__/store/updateOs.test.ts @@ -58,7 +58,7 @@ describe('UpdateOs Store', () => { describe('State and Getters', () => { it('should initialize with correct default values', () => { expect(store.checkForUpdatesLoading).toBe(false); - expect(store.modalOpen).toBe(false); + expect(store.updateOsModalVisible).toBe(false); }); it('should have computed properties with the right types', () => { @@ -86,15 +86,15 @@ describe('UpdateOs Store', () => { await store.localCheckForUpdate(); expect(WebguiCheckForUpdate).toHaveBeenCalled(); - expect(store.modalOpen).toBe(true); + expect(store.updateOsModalVisible).toBe(true); }); it('should set modal open state', () => { store.setModalOpen(true); - expect(store.modalOpen).toBe(true); + expect(store.updateOsModalVisible).toBe(true); store.setModalOpen(false); - expect(store.modalOpen).toBe(false); + expect(store.updateOsModalVisible).toBe(false); }); it('should handle errors when checking for updates', async () => { diff --git a/web/__test__/store/updateOsChangelog.test.ts b/web/__test__/store/updateOsChangelog.test.ts deleted file mode 100644 index 8f3c8a37c..000000000 --- a/web/__test__/store/updateOsChangelog.test.ts +++ /dev/null @@ -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('

Test Title

Test content

'), - }), - }, -})); - -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 = { - 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 = { - ...mockStableRelease, - version: '6.12.5-beta1', -}; - -describe('UpdateOsChangelog Store', () => { - let store: ReturnType; - - 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(); - }); - }); -}); diff --git a/web/_data/serverState.ts b/web/_data/serverState.ts index 17e7da77b..92f2b20ad 100644 --- a/web/_data/serverState.ts +++ b/web/_data/serverState.ts @@ -47,7 +47,7 @@ import type { // EBLACKLISTED2 // 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 regGuid = '1111-1111-YIJD-ZACK1234TEST'; // this guid is registered in key server const keyfileBase64 = ''; diff --git a/web/components/Modal.vue b/web/components/Modal.vue index 55d2440a6..ffb545465 100644 --- a/web/components/Modal.vue +++ b/web/components/Modal.vue @@ -121,7 +121,7 @@ const computedVerticalCenter = computed(() => { ]" 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" > -