mirror of
https://github.com/unraid/api.git
synced 2026-01-06 00:30:22 -06:00
Compare commits
15 Commits
v4.21.0
...
feat/cpu-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fede1b3f5 | ||
|
|
01d353fa08 | ||
|
|
4a07953457 | ||
|
|
0b20e3ea9f | ||
|
|
3f4af09db5 | ||
|
|
222ced7518 | ||
|
|
03dae7ce66 | ||
|
|
0990b898bd | ||
|
|
95faeaa2f3 | ||
|
|
b49ef5a762 | ||
|
|
c782cf0e87 | ||
|
|
f95ca9c9cb | ||
|
|
a59b363ebc | ||
|
|
2fef10c94a | ||
|
|
1c73a4af42 |
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@@ -20,6 +20,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
71
.github/workflows/test-libvirt.yml
vendored
71
.github/workflows/test-libvirt.yml
vendored
@@ -1,71 +0,0 @@
|
||||
name: Test Libvirt
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "libvirt/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- "libvirt/**"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./libvirt
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13.7"
|
||||
|
||||
- name: Cache APT Packages
|
||||
uses: awalsh128/cache-apt-pkgs-action@v1.5.3
|
||||
with:
|
||||
packages: libvirt-dev
|
||||
version: 1.0
|
||||
|
||||
- name: Set Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.15.0
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v4
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('libvirt/package.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: pnpm install
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
|
||||
- name: test
|
||||
run: pnpm run test
|
||||
@@ -1 +1 @@
|
||||
{".":"4.21.0"}
|
||||
{".":"4.22.0"}
|
||||
|
||||
@@ -3,4 +3,3 @@
|
||||
@import './unraid-theme.css';
|
||||
@import './theme-variants.css';
|
||||
@import './base-utilities.css';
|
||||
@import './sonner.css';
|
||||
|
||||
@@ -1,708 +0,0 @@
|
||||
/**------------------------------------------------------------------------------------------------
|
||||
* SONNER.CSS
|
||||
* This is a copy of Sonner's `style.css` as of commit a5b77c2df08d5c05aa923170176168102855533d
|
||||
*
|
||||
* This was necessary because I couldn't find a simple way to include Sonner's styles in vite's
|
||||
* css build output. They wouldn't show up even though the toaster was included, and vue-sonner
|
||||
* currently doesn't export its stylesheet (it appears to be inlined, but styles weren't applied
|
||||
* to the unraid-toaster component for some reason).
|
||||
*------------------------------------------------------------------------------------------------**/
|
||||
:where(html[dir='ltr']),
|
||||
:where([data-sonner-toaster][dir='ltr']) {
|
||||
--toast-icon-margin-start: -3px;
|
||||
--toast-icon-margin-end: 4px;
|
||||
--toast-svg-margin-start: -1px;
|
||||
--toast-svg-margin-end: 0px;
|
||||
--toast-button-margin-start: auto;
|
||||
--toast-button-margin-end: 0;
|
||||
--toast-close-button-start: 0;
|
||||
--toast-close-button-end: unset;
|
||||
--toast-close-button-transform: translate(-35%, -35%);
|
||||
}
|
||||
|
||||
:where(html[dir='rtl']),
|
||||
:where([data-sonner-toaster][dir='rtl']) {
|
||||
--toast-icon-margin-start: 4px;
|
||||
--toast-icon-margin-end: -3px;
|
||||
--toast-svg-margin-start: 0px;
|
||||
--toast-svg-margin-end: -1px;
|
||||
--toast-button-margin-start: 0;
|
||||
--toast-button-margin-end: auto;
|
||||
--toast-close-button-start: unset;
|
||||
--toast-close-button-end: 0;
|
||||
--toast-close-button-transform: translate(35%, -35%);
|
||||
}
|
||||
|
||||
:where([data-sonner-toaster]) {
|
||||
position: fixed;
|
||||
width: var(--width);
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial,
|
||||
Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
|
||||
--gray1: hsl(0, 0%, 99%);
|
||||
--gray2: hsl(0, 0%, 97.3%);
|
||||
--gray3: hsl(0, 0%, 95.1%);
|
||||
--gray4: hsl(0, 0%, 93%);
|
||||
--gray5: hsl(0, 0%, 90.9%);
|
||||
--gray6: hsl(0, 0%, 88.7%);
|
||||
--gray7: hsl(0, 0%, 85.8%);
|
||||
--gray8: hsl(0, 0%, 78%);
|
||||
--gray9: hsl(0, 0%, 56.1%);
|
||||
--gray10: hsl(0, 0%, 52.3%);
|
||||
--gray11: hsl(0, 0%, 43.5%);
|
||||
--gray12: hsl(0, 0%, 9%);
|
||||
--border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
outline: none;
|
||||
z-index: 999999999;
|
||||
transition: transform 400ms ease;
|
||||
}
|
||||
|
||||
:where([data-sonner-toaster][data-lifted='true']) {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
:where([data-sonner-toaster][data-lifted='true']) {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
:where([data-sonner-toaster][data-x-position='right']) {
|
||||
right: max(var(--offset), env(safe-area-inset-right));
|
||||
}
|
||||
|
||||
:where([data-sonner-toaster][data-x-position='left']) {
|
||||
left: max(var(--offset), env(safe-area-inset-left));
|
||||
}
|
||||
|
||||
:where([data-sonner-toaster][data-x-position='center']) {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
:where([data-sonner-toaster][data-y-position='top']) {
|
||||
top: max(var(--offset), env(safe-area-inset-top));
|
||||
}
|
||||
|
||||
:where([data-sonner-toaster][data-y-position='bottom']) {
|
||||
bottom: max(var(--offset), env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
:where([data-sonner-toast]) {
|
||||
--y: translateY(100%);
|
||||
--lift-amount: calc(var(--lift) * var(--gap));
|
||||
z-index: var(--z-index);
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
transform: var(--y);
|
||||
filter: blur(0);
|
||||
/* https://stackoverflow.com/questions/48124372/pointermove-event-not-working-with-touch-why-not */
|
||||
touch-action: none;
|
||||
transition: transform 400ms, opacity 400ms, height 400ms, box-shadow 200ms;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
:where([data-sonner-toast][data-styled='true']) {
|
||||
padding: 16px;
|
||||
background: var(--normal-bg);
|
||||
border: 1px solid var(--normal-border);
|
||||
color: var(--normal-text);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1);
|
||||
width: var(--width);
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
:where([data-sonner-toast]:focus-visible) {
|
||||
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1), 0 0 0 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
:where([data-sonner-toast][data-y-position='top']) {
|
||||
top: 0;
|
||||
--y: translateY(-100%);
|
||||
--lift: 1;
|
||||
--lift-amount: calc(1 * var(--gap));
|
||||
}
|
||||
|
||||
:where([data-sonner-toast][data-y-position='bottom']) {
|
||||
bottom: 0;
|
||||
--y: translateY(100%);
|
||||
--lift: -1;
|
||||
--lift-amount: calc(var(--lift) * var(--gap));
|
||||
}
|
||||
|
||||
:where([data-sonner-toast]) :where([data-description]) {
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
:where([data-sonner-toast]) :where([data-title]) {
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
:where([data-sonner-toast]) :where([data-icon]) {
|
||||
display: flex;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
position: relative;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
margin-left: var(--toast-icon-margin-start);
|
||||
margin-right: var(--toast-icon-margin-end);
|
||||
}
|
||||
|
||||
:where([data-sonner-toast][data-promise='true']) :where([data-icon]) > svg {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
transform-origin: center;
|
||||
animation: sonner-fade-in 300ms ease forwards;
|
||||
}
|
||||
|
||||
:where([data-sonner-toast]) :where([data-icon]) > * {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:where([data-sonner-toast]) :where([data-icon]) svg {
|
||||
margin-left: var(--toast-svg-margin-start);
|
||||
margin-right: var(--toast-svg-margin-end);
|
||||
}
|
||||
|
||||
:where([data-sonner-toast]) :where([data-content]) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
[data-sonner-toast][data-styled='true'] [data-button] {
|
||||
border-radius: 4px;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
height: 24px;
|
||||
font-size: 12px;
|
||||
color: var(--normal-bg);
|
||||
background: var(--normal-text);
|
||||
margin-left: var(--toast-button-margin-start);
|
||||
margin-right: var(--toast-button-margin-end);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
transition: opacity 400ms, box-shadow 200ms;
|
||||
}
|
||||
|
||||
:where([data-sonner-toast]) :where([data-button]):focus-visible {
|
||||
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
:where([data-sonner-toast]) :where([data-button]):first-of-type {
|
||||
margin-left: var(--toast-button-margin-start);
|
||||
margin-right: var(--toast-button-margin-end);
|
||||
}
|
||||
|
||||
:where([data-sonner-toast]) :where([data-cancel]) {
|
||||
color: var(--normal-text);
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
:where([data-sonner-toast][data-theme='dark']) :where([data-cancel]) {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
[data-sonner-toast] [data-close-button] {
|
||||
position: absolute;
|
||||
left: var(--toast-close-button-start);
|
||||
right: var(--toast-close-button-end);
|
||||
top: 0;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
min-width: inherit !important;
|
||||
margin: 0 !important;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
color: hsl(var(--foreground));
|
||||
border: 1px solid hsl(var(--border));
|
||||
transform: var(--toast-close-button-transform);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
transition: opacity 100ms, background 200ms, border-color 200ms;
|
||||
}
|
||||
|
||||
[data-sonner-toast] [data-close-button] {
|
||||
background: hsl(var(--background));
|
||||
}
|
||||
|
||||
:where([data-sonner-toast]) :where([data-close-button]):focus-visible {
|
||||
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1), 0 0 0 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
:where([data-sonner-toast]) :where([data-disabled='true']) {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
[data-sonner-toast]:hover [data-close-button]:hover {
|
||||
background: hsl(var(--muted));
|
||||
border-color: hsl(var(--border));
|
||||
}
|
||||
|
||||
/* Leave a ghost div to avoid setting hover to false when swiping out */
|
||||
:where([data-sonner-toast][data-swiping='true'])::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
:where([data-sonner-toast][data-y-position='top'][data-swiping='true'])::before {
|
||||
/* y 50% needed to distribute height additional height evenly */
|
||||
bottom: 50%;
|
||||
transform: scaleY(3) translateY(50%);
|
||||
}
|
||||
|
||||
:where([data-sonner-toast][data-y-position='bottom'][data-swiping='true'])::before {
|
||||
/* y -50% needed to distribute height additional height evenly */
|
||||
top: 50%;
|
||||
transform: scaleY(3) translateY(-50%);
|
||||
}
|
||||
|
||||
/* Leave a ghost div to avoid setting hover to false when transitioning out */
|
||||
:where([data-sonner-toast][data-swiping='false'][data-removed='true'])::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
transform: scaleY(2);
|
||||
}
|
||||
|
||||
/* Needed to avoid setting hover to false when inbetween toasts */
|
||||
:where([data-sonner-toast])::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
height: calc(var(--gap) + 1px);
|
||||
bottom: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:where([data-sonner-toast][data-mounted='true']) {
|
||||
--y: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
:where([data-sonner-toast][data-expanded='false'][data-front='false']) {
|
||||
--scale: var(--toasts-before) * 0.05 + 1;
|
||||
--y: translateY(calc(var(--lift-amount) * var(--toasts-before))) scale(calc(-1 * var(--scale)));
|
||||
height: var(--front-toast-height);
|
||||
}
|
||||
|
||||
:where([data-sonner-toast]) > * {
|
||||
transition: opacity 400ms;
|
||||
}
|
||||
|
||||
:where([data-sonner-toast][data-expanded='false'][data-front='false'][data-styled='true']) > * {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
:where([data-sonner-toast][data-visible='false']) {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:where([data-sonner-toast][data-mounted='true'][data-expanded='true']) {
|
||||
--y: translateY(calc(var(--lift) * var(--offset)));
|
||||
height: var(--initial-height);
|
||||
}
|
||||
|
||||
:where([data-sonner-toast][data-removed='true'][data-front='true'][data-swipe-out='false']) {
|
||||
--y: translateY(calc(var(--lift) * -100%));
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
:where([data-sonner-toast][data-removed='true'][data-front='false'][data-swipe-out='false'][data-expanded='true']) {
|
||||
--y: translateY(calc(var(--lift) * var(--offset) + var(--lift) * -100%));
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
:where([data-sonner-toast][data-removed='true'][data-front='false'][data-swipe-out='false'][data-expanded='false']) {
|
||||
--y: translateY(40%);
|
||||
opacity: 0;
|
||||
transition: transform 500ms, opacity 200ms;
|
||||
}
|
||||
|
||||
/* Bump up the height to make sure hover state doesn't get set to false */
|
||||
:where([data-sonner-toast][data-removed='true'][data-front='false'])::before {
|
||||
height: calc(var(--initial-height) + 20%);
|
||||
}
|
||||
|
||||
[data-sonner-toast][data-swiping='true'] {
|
||||
transform: var(--y) translateY(var(--swipe-amount, 0px));
|
||||
transition: none;
|
||||
}
|
||||
|
||||
[data-sonner-toast][data-swiped='true'] {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
[data-sonner-toast][data-swipe-out='true'][data-y-position='bottom'],
|
||||
[data-sonner-toast][data-swipe-out='true'][data-y-position='top'] {
|
||||
animation: swipe-out 200ms ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes swipe-out {
|
||||
from {
|
||||
transform: translateY(calc(var(--lift) * var(--offset) + var(--swipe-amount)));
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(calc(var(--lift) * var(--offset) + var(--swipe-amount) + var(--lift) * -100%));
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
[data-sonner-toaster] {
|
||||
position: fixed;
|
||||
--mobile-offset: 16px;
|
||||
right: var(--mobile-offset);
|
||||
left: var(--mobile-offset);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[data-sonner-toaster][dir='rtl'] {
|
||||
left: calc(var(--mobile-offset) * -1);
|
||||
}
|
||||
|
||||
[data-sonner-toaster] [data-sonner-toast] {
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: calc(100% - var(--mobile-offset) * 2);
|
||||
}
|
||||
|
||||
[data-sonner-toaster][data-x-position='left'] {
|
||||
left: var(--mobile-offset);
|
||||
}
|
||||
|
||||
[data-sonner-toaster][data-y-position='bottom'] {
|
||||
bottom: 20px;
|
||||
}
|
||||
|
||||
[data-sonner-toaster][data-y-position='top'] {
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
[data-sonner-toaster][data-x-position='center'] {
|
||||
left: var(--mobile-offset);
|
||||
right: var(--mobile-offset);
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-sonner-toaster][data-theme='light'] {
|
||||
--normal-bg: hsl(var(--background));
|
||||
--normal-border: hsl(var(--border));
|
||||
--normal-text: hsl(var(--foreground));
|
||||
|
||||
--success-bg: hsl(var(--background));
|
||||
--success-border: hsl(var(--border));
|
||||
--success-text: hsl(140, 100%, 27%);
|
||||
|
||||
--info-bg: hsl(var(--background));
|
||||
--info-border: hsl(var(--border));
|
||||
--info-text: hsl(210, 92%, 45%);
|
||||
|
||||
--warning-bg: hsl(var(--background));
|
||||
--warning-border: hsl(var(--border));
|
||||
--warning-text: hsl(31, 92%, 45%);
|
||||
|
||||
--error-bg: hsl(var(--background));
|
||||
--error-border: hsl(var(--border));
|
||||
--error-text: hsl(360, 100%, 45%);
|
||||
|
||||
/* Old colors, preserved for reference
|
||||
--success-bg: hsl(143, 85%, 96%);
|
||||
--success-border: hsl(145, 92%, 91%);
|
||||
--success-text: hsl(140, 100%, 27%);
|
||||
|
||||
--info-bg: hsl(208, 100%, 97%);
|
||||
--info-border: hsl(221, 91%, 91%);
|
||||
--info-text: hsl(210, 92%, 45%);
|
||||
|
||||
--warning-bg: hsl(49, 100%, 97%);
|
||||
--warning-border: hsl(49, 91%, 91%);
|
||||
--warning-text: hsl(31, 92%, 45%);
|
||||
|
||||
--error-bg: hsl(359, 100%, 97%);
|
||||
--error-border: hsl(359, 100%, 94%);
|
||||
--error-text: hsl(360, 100%, 45%); */
|
||||
}
|
||||
|
||||
[data-sonner-toaster][data-theme='light'] [data-sonner-toast][data-invert='true'] {
|
||||
--normal-bg: hsl(0 0% 3.9%);
|
||||
--normal-border: hsl(0 0% 14.9%);
|
||||
--normal-text: hsl(0 0% 98%);
|
||||
}
|
||||
|
||||
[data-sonner-toaster][data-theme='dark'] [data-sonner-toast][data-invert='true'] {
|
||||
--normal-bg: hsl(0 0% 100%);
|
||||
--normal-border: hsl(0 0% 89.8%);
|
||||
--normal-text: hsl(0 0% 3.9%);
|
||||
}
|
||||
|
||||
[data-sonner-toaster][data-theme='dark'] {
|
||||
--normal-bg: hsl(var(--background));
|
||||
--normal-border: hsl(var(--border));
|
||||
--normal-text: hsl(var(--foreground));
|
||||
|
||||
--success-bg: hsl(var(--background));
|
||||
--success-border: hsl(var(--border));
|
||||
--success-text: hsl(150, 86%, 65%);
|
||||
|
||||
--info-bg: hsl(var(--background));
|
||||
--info-border: hsl(var(--border));
|
||||
--info-text: hsl(216, 87%, 65%);
|
||||
|
||||
--warning-bg: hsl(var(--background));
|
||||
--warning-border: hsl(var(--border));
|
||||
--warning-text: hsl(46, 87%, 65%);
|
||||
|
||||
--error-bg: hsl(var(--background));
|
||||
--error-border: hsl(var(--border));
|
||||
--error-text: hsl(358, 100%, 81%);
|
||||
|
||||
/* Old colors, preserved for reference
|
||||
--success-bg: hsl(150, 100%, 6%);
|
||||
--success-border: hsl(147, 100%, 12%);
|
||||
--success-text: hsl(150, 86%, 65%);
|
||||
|
||||
--info-bg: hsl(215, 100%, 6%);
|
||||
--info-border: hsl(223, 100%, 12%);
|
||||
--info-text: hsl(216, 87%, 65%);
|
||||
|
||||
--warning-bg: hsl(64, 100%, 6%);
|
||||
--warning-border: hsl(60, 100%, 12%);
|
||||
--warning-text: hsl(46, 87%, 65%);
|
||||
|
||||
--error-bg: hsl(358, 76%, 10%);
|
||||
--error-border: hsl(357, 89%, 16%);
|
||||
--error-text: hsl(358, 100%, 81%); */
|
||||
}
|
||||
|
||||
[data-rich-colors='true'][data-sonner-toast][data-type='success'] {
|
||||
background: var(--success-bg);
|
||||
border-color: var(--success-border);
|
||||
color: var(--success-text);
|
||||
}
|
||||
|
||||
[data-rich-colors='true'][data-sonner-toast][data-type='success'] [data-close-button] {
|
||||
background: var(--success-bg);
|
||||
border-color: var(--success-border);
|
||||
color: var(--success-text);
|
||||
}
|
||||
|
||||
[data-rich-colors='true'][data-sonner-toast][data-type='info'] {
|
||||
background: var(--info-bg);
|
||||
border-color: var(--info-border);
|
||||
color: var(--info-text);
|
||||
}
|
||||
|
||||
[data-rich-colors='true'][data-sonner-toast][data-type='info'] [data-close-button] {
|
||||
background: var(--info-bg);
|
||||
border-color: var(--info-border);
|
||||
color: var(--info-text);
|
||||
}
|
||||
|
||||
[data-rich-colors='true'][data-sonner-toast][data-type='warning'] {
|
||||
background: var(--warning-bg);
|
||||
border-color: var(--warning-border);
|
||||
color: var(--warning-text);
|
||||
}
|
||||
|
||||
[data-rich-colors='true'][data-sonner-toast][data-type='warning'] [data-close-button] {
|
||||
background: var(--warning-bg);
|
||||
border-color: var(--warning-border);
|
||||
color: var(--warning-text);
|
||||
}
|
||||
|
||||
[data-rich-colors='true'][data-sonner-toast][data-type='error'] {
|
||||
background: var(--error-bg);
|
||||
border-color: var(--error-border);
|
||||
color: var(--error-text);
|
||||
}
|
||||
|
||||
[data-rich-colors='true'][data-sonner-toast][data-type='error'] [data-close-button] {
|
||||
background: var(--error-bg);
|
||||
border-color: var(--error-border);
|
||||
color: var(--error-text);
|
||||
}
|
||||
|
||||
.sonner-loading-wrapper {
|
||||
--size: 16px;
|
||||
height: var(--size);
|
||||
width: var(--size);
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.sonner-loading-wrapper[data-visible='false'] {
|
||||
transform-origin: center;
|
||||
animation: sonner-fade-out 0.2s ease forwards;
|
||||
}
|
||||
|
||||
.sonner-spinner {
|
||||
position: relative;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
height: var(--size);
|
||||
width: var(--size);
|
||||
}
|
||||
|
||||
.sonner-loading-bar {
|
||||
animation: sonner-spin 1.2s linear infinite;
|
||||
background: hsl(var(--muted-foreground));
|
||||
border-radius: 6px;
|
||||
height: 8%;
|
||||
left: -10%;
|
||||
position: absolute;
|
||||
top: -3.9%;
|
||||
width: 24%;
|
||||
}
|
||||
|
||||
.sonner-loading-bar:nth-child(1) {
|
||||
animation-delay: -1.2s;
|
||||
transform: rotate(0.0001deg) translate(146%);
|
||||
}
|
||||
|
||||
.sonner-loading-bar:nth-child(2) {
|
||||
animation-delay: -1.1s;
|
||||
transform: rotate(30deg) translate(146%);
|
||||
}
|
||||
|
||||
.sonner-loading-bar:nth-child(3) {
|
||||
animation-delay: -1s;
|
||||
transform: rotate(60deg) translate(146%);
|
||||
}
|
||||
|
||||
.sonner-loading-bar:nth-child(4) {
|
||||
animation-delay: -0.9s;
|
||||
transform: rotate(90deg) translate(146%);
|
||||
}
|
||||
|
||||
.sonner-loading-bar:nth-child(5) {
|
||||
animation-delay: -0.8s;
|
||||
transform: rotate(120deg) translate(146%);
|
||||
}
|
||||
|
||||
.sonner-loading-bar:nth-child(6) {
|
||||
animation-delay: -0.7s;
|
||||
transform: rotate(150deg) translate(146%);
|
||||
}
|
||||
|
||||
.sonner-loading-bar:nth-child(7) {
|
||||
animation-delay: -0.6s;
|
||||
transform: rotate(180deg) translate(146%);
|
||||
}
|
||||
|
||||
.sonner-loading-bar:nth-child(8) {
|
||||
animation-delay: -0.5s;
|
||||
transform: rotate(210deg) translate(146%);
|
||||
}
|
||||
|
||||
.sonner-loading-bar:nth-child(9) {
|
||||
animation-delay: -0.4s;
|
||||
transform: rotate(240deg) translate(146%);
|
||||
}
|
||||
|
||||
.sonner-loading-bar:nth-child(10) {
|
||||
animation-delay: -0.3s;
|
||||
transform: rotate(270deg) translate(146%);
|
||||
}
|
||||
|
||||
.sonner-loading-bar:nth-child(11) {
|
||||
animation-delay: -0.2s;
|
||||
transform: rotate(300deg) translate(146%);
|
||||
}
|
||||
|
||||
.sonner-loading-bar:nth-child(12) {
|
||||
animation-delay: -0.1s;
|
||||
transform: rotate(330deg) translate(146%);
|
||||
}
|
||||
|
||||
@keyframes sonner-fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sonner-fade-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sonner-spin {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.15;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
[data-sonner-toast],
|
||||
[data-sonner-toast] > *,
|
||||
.sonner-loading-bar {
|
||||
transition: none !important;
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.sonner-loader {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
transform-origin: center;
|
||||
transition: opacity 200ms, transform 200ms;
|
||||
}
|
||||
|
||||
.sonner-loader[data-visible='false'] {
|
||||
opacity: 0;
|
||||
transform: scale(0.8) translate(-50%, -50%);
|
||||
}
|
||||
|
||||
/* Override Unraid webgui docker icon styles on sonner containers */
|
||||
[data-sonner-toast] [data-icon]:before,
|
||||
[data-sonner-toast] .fa-docker:before {
|
||||
font-family: inherit !important;
|
||||
content: '' !important;
|
||||
}
|
||||
@@ -7,7 +7,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
This is the Unraid API monorepo containing multiple packages that provide API functionality for Unraid servers. It uses pnpm workspaces with the following structure:
|
||||
|
||||
- `/api` - Core NestJS API server with GraphQL
|
||||
- `/web` - Nuxt.js frontend application
|
||||
- `/web` - Vue 3 frontend application
|
||||
- `/unraid-ui` - Vue 3 component library
|
||||
- `/plugin` - Unraid plugin package (.plg)
|
||||
- `/packages` - Shared packages and API plugins
|
||||
@@ -128,9 +128,6 @@ Enables GraphQL playground at `http://tower.local/graphql`
|
||||
- **Use Mocks Correctly**: Mocks should be used as nouns, not verbs.
|
||||
|
||||
#### Vue Component Testing
|
||||
|
||||
- This is a Nuxt.js app but we are testing with vitest outside of the Nuxt environment
|
||||
- Nuxt is currently set to auto import so some vue files may need compute or ref imported
|
||||
- Use pnpm when running terminal commands and stay within the web directory
|
||||
- Tests are located under `web/__test__`, run with `pnpm test`
|
||||
- Use `mount` from Vue Test Utils for component testing
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# Changelog
|
||||
|
||||
## [4.22.0](https://github.com/unraid/api/compare/v4.21.0...v4.22.0) (2025-09-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* improved update ui ([#1691](https://github.com/unraid/api/issues/1691)) ([a59b363](https://github.com/unraid/api/commit/a59b363ebc1e660f854c55d50fc02c823c2fd0cc))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update dependency camelcase-keys to v10 ([#1687](https://github.com/unraid/api/issues/1687)) ([95faeaa](https://github.com/unraid/api/commit/95faeaa2f39bf7bd16502698d7530aaa590b286d))
|
||||
* **deps:** update dependency p-retry to v7 ([#1608](https://github.com/unraid/api/issues/1608)) ([c782cf0](https://github.com/unraid/api/commit/c782cf0e8710c6690050376feefda3edb30dd549))
|
||||
* **deps:** update dependency uuid to v13 ([#1688](https://github.com/unraid/api/issues/1688)) ([2fef10c](https://github.com/unraid/api/commit/2fef10c94aae910e95d9f5bcacf7289e2cca6ed9))
|
||||
* **deps:** update dependency vue-sonner to v2 ([#1475](https://github.com/unraid/api/issues/1475)) ([f95ca9c](https://github.com/unraid/api/commit/f95ca9c9cb69725dcf3bb4bcbd0b558a2074e311))
|
||||
* display settings fix for languages on less than 7.2-beta.2.3 ([#1696](https://github.com/unraid/api/issues/1696)) ([03dae7c](https://github.com/unraid/api/commit/03dae7ce66b3409593eeee90cd5b56e2a920ca44))
|
||||
* hide reset help option when sso is being checked ([#1695](https://github.com/unraid/api/issues/1695)) ([222ced7](https://github.com/unraid/api/commit/222ced7518d40c207198a3b8548f0e024bc865b0))
|
||||
* progressFrame white on black ([0990b89](https://github.com/unraid/api/commit/0990b898bd02c231153157c20d5142e5fd4513cd))
|
||||
|
||||
## [4.21.0](https://github.com/unraid/api/compare/v4.20.4...v4.21.0) (2025-09-10)
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "4.19.1",
|
||||
"version": "4.22.0",
|
||||
"extraOrigins": [],
|
||||
"sandbox": true,
|
||||
"ssoSubIds": [],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@unraid/api",
|
||||
"version": "4.21.0",
|
||||
"version": "4.22.0",
|
||||
"main": "src/cli/index.ts",
|
||||
"type": "module",
|
||||
"corepack": {
|
||||
@@ -84,7 +84,7 @@
|
||||
"bytes": "3.1.2",
|
||||
"cache-manager": "7.2.0",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"camelcase-keys": "9.1.3",
|
||||
"camelcase-keys": "10.0.0",
|
||||
"casbin": "5.38.0",
|
||||
"change-case": "5.4.4",
|
||||
"chokidar": "4.0.3",
|
||||
@@ -127,7 +127,7 @@
|
||||
"node-cache": "5.1.2",
|
||||
"node-window-polyfill": "1.0.4",
|
||||
"openid-client": "6.6.4",
|
||||
"p-retry": "6.2.1",
|
||||
"p-retry": "7.0.0",
|
||||
"passport-custom": "1.1.1",
|
||||
"passport-http-header-strategy": "1.1.0",
|
||||
"path-type": "6.0.0",
|
||||
@@ -141,7 +141,7 @@
|
||||
"strftime": "0.10.3",
|
||||
"systeminformation": "5.27.8",
|
||||
"undici": "7.15.0",
|
||||
"uuid": "11.1.0",
|
||||
"uuid": "13.0.0",
|
||||
"ws": "8.18.3",
|
||||
"zen-observable-ts": "1.1.0",
|
||||
"zod": "3.25.76"
|
||||
|
||||
@@ -29,16 +29,16 @@ export class CpuService {
|
||||
|
||||
return {
|
||||
id: 'info/cpu-load',
|
||||
percentTotal: loadData.currentLoad,
|
||||
percentTotal: Math.floor(loadData.currentLoad),
|
||||
cpus: loadData.cpus.map((cpu) => ({
|
||||
percentTotal: cpu.load,
|
||||
percentUser: cpu.loadUser,
|
||||
percentSystem: cpu.loadSystem,
|
||||
percentNice: cpu.loadNice,
|
||||
percentIdle: cpu.loadIdle,
|
||||
percentIrq: cpu.loadIrq,
|
||||
percentGuest: cpu.loadGuest || 0,
|
||||
percentSteal: cpu.loadSteal || 0,
|
||||
percentTotal: Math.floor(cpu.load),
|
||||
percentUser: Math.floor(cpu.loadUser),
|
||||
percentSystem: Math.floor(cpu.loadSystem),
|
||||
percentNice: Math.floor(cpu.loadNice),
|
||||
percentIdle: Math.floor(cpu.loadIdle),
|
||||
percentIrq: Math.floor(cpu.loadIrq),
|
||||
percentGuest: Math.floor(cpu.loadGuest || 0),
|
||||
percentSteal: Math.floor(cpu.loadSteal || 0),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
|
||||
import { FileModification } from '@app/unraid-api/unraid-file-modifier/file-modification.js';
|
||||
import {
|
||||
FileModification,
|
||||
ShouldApplyWithReason,
|
||||
} from '@app/unraid-api/unraid-file-modifier/file-modification.js';
|
||||
|
||||
export default class DisplaySettingsModification extends FileModification {
|
||||
id: string = 'display-settings';
|
||||
@@ -34,4 +37,15 @@ export default class DisplaySettingsModification extends FileModification {
|
||||
|
||||
return this.createPatchWithDiff(overridePath ?? this.filePath, fileContent, newContent);
|
||||
}
|
||||
|
||||
async shouldApply(): Promise<ShouldApplyWithReason> {
|
||||
const superShouldApply = await super.shouldApply();
|
||||
if (!superShouldApply.shouldApply) {
|
||||
return superShouldApply;
|
||||
}
|
||||
return {
|
||||
shouldApply: true,
|
||||
reason: 'Display settings modification needed for Unraid version <= 7.2.0-beta.2.3',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "unraid-monorepo",
|
||||
"private": true,
|
||||
"version": "4.21.0",
|
||||
"version": "4.22.0",
|
||||
"scripts": {
|
||||
"build": "pnpm -r build",
|
||||
"build:watch": "pnpm -r --parallel --filter '!@unraid/ui' build:watch",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"description": "Unraid Connect plugin for Unraid API",
|
||||
"devDependencies": {
|
||||
"@apollo/client": "3.14.0",
|
||||
"@faker-js/faker": "9.9.0",
|
||||
"@faker-js/faker": "10.0.0",
|
||||
"@graphql-codegen/cli": "5.0.7",
|
||||
"@graphql-typed-document-node/core": "3.2.0",
|
||||
"@ianvs/prettier-plugin-sort-imports": "4.6.3",
|
||||
@@ -43,7 +43,7 @@
|
||||
"@types/lodash-es": "4.17.12",
|
||||
"@types/node": "22.18.0",
|
||||
"@types/ws": "8.18.1",
|
||||
"camelcase-keys": "9.1.3",
|
||||
"camelcase-keys": "10.0.0",
|
||||
"class-transformer": "0.5.1",
|
||||
"class-validator": "0.14.2",
|
||||
"execa": "9.6.0",
|
||||
@@ -84,7 +84,7 @@
|
||||
"@nestjs/graphql": "13.1.0",
|
||||
"@nestjs/schedule": "6.0.0",
|
||||
"@runonflux/nat-upnp": "1.0.2",
|
||||
"camelcase-keys": "9.1.3",
|
||||
"camelcase-keys": "10.0.0",
|
||||
"class-transformer": "0.5.1",
|
||||
"class-validator": "0.14.2",
|
||||
"execa": "9.6.0",
|
||||
|
||||
181
plugin/builder/utils/changelog.test.ts
Normal file
181
plugin/builder/utils/changelog.test.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { describe, it, expect, beforeAll } from "vitest";
|
||||
import { execSync } from "child_process";
|
||||
import { getStagingChangelogFromGit } from "./changelog.js";
|
||||
|
||||
describe.sequential("getStagingChangelogFromGit", () => {
|
||||
let currentCommitMessage: string | null = null;
|
||||
|
||||
beforeAll(() => {
|
||||
// Get the current commit message to validate it appears in changelog
|
||||
try {
|
||||
currentCommitMessage = execSync('git log -1 --pretty=%s', { encoding: 'utf8' }).trim();
|
||||
} catch (e) {
|
||||
// Ignore if we can't get commit
|
||||
}
|
||||
});
|
||||
|
||||
it("should generate changelog header with version", { timeout: 20000 }, async () => {
|
||||
const result = await getStagingChangelogFromGit({
|
||||
pluginVersion: "99.99.99",
|
||||
tag: undefined as any,
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result).toBe("string");
|
||||
// Should contain version header
|
||||
expect(result).toContain("99.99.99");
|
||||
// Should have markdown header formatting
|
||||
expect(result).toMatch(/##\s+/);
|
||||
});
|
||||
|
||||
it("should generate changelog with tag parameter", { timeout: 20000 }, async () => {
|
||||
// When tag is provided, it should generate changelog with tag in header
|
||||
const result = await getStagingChangelogFromGit({
|
||||
pluginVersion: "99.99.99",
|
||||
tag: "test-tag-99",
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result).toBe("string");
|
||||
expect(result).toContain("test-tag-99");
|
||||
|
||||
// Should have a version header
|
||||
expect(result).toMatch(/##\s+/);
|
||||
|
||||
// IMPORTANT: Verify that actual commits are included in the changelog
|
||||
// This ensures the gitRawCommitsOpts is working correctly
|
||||
// The changelog should include commits if there are any between origin/main and HEAD
|
||||
// We check for common changelog patterns that indicate actual content
|
||||
if (result.length > 100) {
|
||||
// If we have a substantial changelog, it should contain commit information
|
||||
expect(
|
||||
result.includes("### Features") ||
|
||||
result.includes("### Bug Fixes") ||
|
||||
result.includes("### ") ||
|
||||
result.includes("* ") // Commit entries typically start with asterisk
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("should handle error gracefully and return tag", { timeout: 20000 }, async () => {
|
||||
// The function catches errors and returns the tag
|
||||
// An empty version might not cause an error, so let's just verify
|
||||
// the function completes without throwing
|
||||
const result = await getStagingChangelogFromGit({
|
||||
pluginVersion: "test-version",
|
||||
tag: "fallback-tag",
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result).toBe("string");
|
||||
// Should either return a changelog or the fallback tag
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should use conventional-changelog v7 API correctly", { timeout: 20000 }, async () => {
|
||||
// This test validates that the v7 API is being called correctly
|
||||
// by checking that the function executes without throwing
|
||||
let error: any = null;
|
||||
|
||||
try {
|
||||
await getStagingChangelogFromGit({
|
||||
pluginVersion: "99.99.99",
|
||||
tag: undefined as any,
|
||||
});
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
// The v7 API should work without errors
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
|
||||
it("should validate changelog structure", { timeout: 20000 }, async () => {
|
||||
// Create a changelog with high version number to avoid conflicts
|
||||
const result = await getStagingChangelogFromGit({
|
||||
pluginVersion: "999.0.0",
|
||||
tag: "v999-test",
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result).toBe("string");
|
||||
|
||||
// Verify basic markdown structure
|
||||
if (result.length > 50) {
|
||||
// Should have tag in header when tag is provided
|
||||
expect(result).toMatch(/##\s+\[?v999-test/);
|
||||
// Should be valid markdown with proper line breaks
|
||||
expect(result).toMatch(/\n/);
|
||||
}
|
||||
});
|
||||
|
||||
it("should include actual commits when using gitRawCommitsOpts with tag", { timeout: 20000 }, async () => {
|
||||
// This test ensures that gitRawCommitsOpts is working correctly
|
||||
// and actually fetching commits between origin/main and HEAD
|
||||
const result = await getStagingChangelogFromGit({
|
||||
pluginVersion: "99.99.99",
|
||||
tag: "CI-TEST",
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result).toBe("string");
|
||||
|
||||
// The header should contain the tag
|
||||
expect(result).toContain("CI-TEST");
|
||||
|
||||
// Critical: The changelog should NOT be just the tag (error fallback)
|
||||
expect(result).not.toBe("CI-TEST");
|
||||
|
||||
// The changelog should have a proper markdown header
|
||||
expect(result).toMatch(/^##\s+/);
|
||||
|
||||
// Check if we're in a git repo with commits ahead of the base branch
|
||||
let commitCount = 0;
|
||||
try {
|
||||
// Try to detect the base branch (same logic as in changelog.ts)
|
||||
let baseBranch = "origin/main";
|
||||
try {
|
||||
const originHead = execSync("git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null", {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"]
|
||||
}).trim();
|
||||
if (originHead) {
|
||||
baseBranch = originHead.replace("refs/remotes/", "");
|
||||
}
|
||||
} catch {
|
||||
// Try common branches
|
||||
const branches = ["origin/main", "origin/master", "origin/develop"];
|
||||
for (const branch of branches) {
|
||||
try {
|
||||
execSync(`git rev-parse --verify ${branch} 2>/dev/null`, { stdio: "ignore" });
|
||||
baseBranch = branch;
|
||||
break;
|
||||
} catch {
|
||||
// Continue to next branch
|
||||
}
|
||||
}
|
||||
}
|
||||
commitCount = parseInt(execSync(`git rev-list --count ${baseBranch}..HEAD`, { encoding: "utf8" }).trim());
|
||||
} catch {
|
||||
// If we can't determine, we'll check for minimal content
|
||||
}
|
||||
|
||||
// If there are commits on this branch, the changelog MUST include them
|
||||
if (commitCount > 0) {
|
||||
// The changelog must be more than just a header
|
||||
// A minimal header is "## CI-TEST (2025-09-12)\n\n" which is ~30 chars
|
||||
expect(result.length).toBeGreaterThan(50);
|
||||
|
||||
// Should have actual commit content
|
||||
const hasCommitContent =
|
||||
result.includes("### ") || // Section headers like ### Features
|
||||
result.includes("* ") || // Commit bullet points
|
||||
result.includes("- "); // Alternative bullet style
|
||||
|
||||
if (!hasCommitContent) {
|
||||
throw new Error(`Expected changelog to contain commits but got only: ${result.substring(0, 100)}...`);
|
||||
}
|
||||
expect(hasCommitContent).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,40 +1,167 @@
|
||||
import conventionalChangelog from "conventional-changelog";
|
||||
import { ConventionalChangelog } from "conventional-changelog";
|
||||
import { execSync } from "child_process";
|
||||
|
||||
import { PluginEnv } from "../cli/setup-plugin-environment";
|
||||
import { PluginEnv } from "../cli/setup-plugin-environment.js";
|
||||
|
||||
/**
|
||||
* Detects the base branch and finds the merge base for PR changelog generation
|
||||
* Returns the merge-base commit to only show commits from the current PR
|
||||
*/
|
||||
function getMergeBase(): string | null {
|
||||
try {
|
||||
// First, find the base branch
|
||||
let baseBranch: string | null = null;
|
||||
|
||||
// Try to get the default branch from origin/HEAD
|
||||
try {
|
||||
const originHead = execSync("git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null", {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"]
|
||||
}).trim();
|
||||
if (originHead) {
|
||||
baseBranch = originHead.replace("refs/remotes/", "");
|
||||
}
|
||||
} catch {
|
||||
// origin/HEAD not set, continue to next strategy
|
||||
}
|
||||
|
||||
// Try common default branch names if origin/HEAD didn't work
|
||||
if (!baseBranch) {
|
||||
const commonBranches = ["origin/main", "origin/master", "origin/develop"];
|
||||
for (const branch of commonBranches) {
|
||||
try {
|
||||
execSync(`git rev-parse --verify ${branch} 2>/dev/null`, { stdio: "ignore" });
|
||||
baseBranch = branch;
|
||||
break;
|
||||
} catch {
|
||||
// Branch doesn't exist, try next
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!baseBranch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find the merge-base between the current branch and the base branch
|
||||
// This gives us the commit where the PR branch diverged from main
|
||||
try {
|
||||
const mergeBase = execSync(`git merge-base ${baseBranch} HEAD`, {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"]
|
||||
}).trim();
|
||||
|
||||
return mergeBase;
|
||||
} catch {
|
||||
// If merge-base fails, fall back to the base branch itself
|
||||
return baseBranch;
|
||||
}
|
||||
} catch {
|
||||
// Git command failed entirely, return null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a simple changelog for PR builds
|
||||
*/
|
||||
function generatePRChangelog(tag: string, mergeBase: string): string | null {
|
||||
try {
|
||||
// Get commits from this PR only with conventional commit parsing
|
||||
const commits = execSync(
|
||||
`git log ${mergeBase}..HEAD --pretty=format:"%s|%h" --reverse`,
|
||||
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }
|
||||
).trim();
|
||||
|
||||
if (!commits) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lines = commits.split('\n').filter(Boolean);
|
||||
const features: string[] = [];
|
||||
const fixes: string[] = [];
|
||||
const other: string[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const [message, hash] = line.split('|');
|
||||
const formatted = `* ${message} (${hash})`;
|
||||
|
||||
if (message.startsWith('feat')) {
|
||||
features.push(formatted);
|
||||
} else if (message.startsWith('fix')) {
|
||||
fixes.push(formatted);
|
||||
} else {
|
||||
other.push(formatted);
|
||||
}
|
||||
}
|
||||
|
||||
let changelog = `## [${tag}](https://github.com/unraid/api/${tag})\n\n`;
|
||||
|
||||
if (features.length > 0) {
|
||||
changelog += `### Features\n\n${features.join('\n')}\n\n`;
|
||||
}
|
||||
if (fixes.length > 0) {
|
||||
changelog += `### Bug Fixes\n\n${fixes.join('\n')}\n\n`;
|
||||
}
|
||||
if (other.length > 0) {
|
||||
changelog += `### Other Changes\n\n${other.join('\n')}\n\n`;
|
||||
}
|
||||
|
||||
return changelog;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const getStagingChangelogFromGit = async ({
|
||||
pluginVersion,
|
||||
tag,
|
||||
}: Pick<PluginEnv, "pluginVersion" | "tag">): Promise<string> => {
|
||||
try {
|
||||
const changelogStream = conventionalChangelog(
|
||||
{
|
||||
preset: "conventionalcommits",
|
||||
},
|
||||
{
|
||||
version: pluginVersion,
|
||||
},
|
||||
tag
|
||||
? {
|
||||
from: "origin/main",
|
||||
to: "HEAD",
|
||||
}
|
||||
: {},
|
||||
undefined,
|
||||
tag
|
||||
? {
|
||||
headerPartial: `## [${tag}](https://github.com/unraid/api/${tag})\n\n`,
|
||||
}
|
||||
: undefined
|
||||
);
|
||||
// For PR builds with a tag, try to generate a simple PR-specific changelog
|
||||
if (tag) {
|
||||
const mergeBase = getMergeBase();
|
||||
if (mergeBase) {
|
||||
const prChangelog = generatePRChangelog(tag, mergeBase);
|
||||
if (prChangelog) {
|
||||
return prChangelog;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to conventional-changelog for non-PR builds or if PR detection fails
|
||||
const options: any = {
|
||||
releaseCount: 1,
|
||||
};
|
||||
|
||||
if (tag) {
|
||||
options.writerOpts = {
|
||||
headerPartial: `## [${tag}](https://github.com/unraid/api/${tag})\n\n`,
|
||||
};
|
||||
}
|
||||
|
||||
const generator = new ConventionalChangelog()
|
||||
.loadPreset("conventionalcommits")
|
||||
.context({
|
||||
version: tag || pluginVersion,
|
||||
...(tag && {
|
||||
linkCompare: false,
|
||||
}),
|
||||
})
|
||||
.options(options);
|
||||
|
||||
let changelog = "";
|
||||
for await (const chunk of changelogStream) {
|
||||
for await (const chunk of generator.write()) {
|
||||
changelog += chunk;
|
||||
}
|
||||
// Encode HTML entities using the 'he' library
|
||||
return changelog ?? "";
|
||||
|
||||
return changelog || "";
|
||||
} catch (err) {
|
||||
console.log('Non-fatal error: Failed to get changelog from git:', err);
|
||||
return tag;
|
||||
// Return a properly formatted fallback with markdown header
|
||||
if (tag) {
|
||||
return `## [${tag}](https://github.com/unraid/api/${tag})\n\n`;
|
||||
}
|
||||
return `## ${pluginVersion}\n\n`;
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"name": "@unraid/connect-plugin",
|
||||
"version": "4.21.0",
|
||||
"version": "4.22.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"commander": "14.0.0",
|
||||
"conventional-changelog": "6.0.0",
|
||||
"conventional-changelog": "7.1.1",
|
||||
"conventional-changelog-conventionalcommits": "^9.1.0",
|
||||
"date-fns": "4.1.0",
|
||||
"glob": "11.0.3",
|
||||
"html-sloppy-escaper": "0.1.0",
|
||||
|
||||
4034
pnpm-lock.yaml
generated
4034
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@unraid/ui",
|
||||
"version": "4.21.0",
|
||||
"version": "4.22.0",
|
||||
"private": true,
|
||||
"license": "GPL-2.0-or-later",
|
||||
"type": "module",
|
||||
@@ -66,7 +66,7 @@
|
||||
"shadcn-vue": "2.2.0",
|
||||
"tailwind-merge": "2.6.0",
|
||||
"tw-animate-css": "1.3.7",
|
||||
"vue-sonner": "1.3.2"
|
||||
"vue-sonner": "2.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "9.34.0",
|
||||
|
||||
@@ -51,7 +51,7 @@ const classes = computed(() => {
|
||||
});
|
||||
|
||||
const needsBrandGradientBackground = computed(() => {
|
||||
return ['outline-solid', 'outline-primary'].includes(props.variant ?? '');
|
||||
return ['outline', 'outline-solid', 'outline-primary'].includes(props.variant ?? '');
|
||||
});
|
||||
|
||||
const isLink = computed(() => Boolean(props.href));
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
import { Toaster as Sonner, toast, type ToasterProps } from 'vue-sonner';
|
||||
import 'vue-sonner/style.css';
|
||||
|
||||
const props = defineProps<ToasterProps>();
|
||||
// Accept theme as a prop, default to 'light' if not provided
|
||||
interface Props extends ToasterProps {
|
||||
theme?: 'light' | 'dark' | 'system';
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
theme: 'light',
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
globalThis.toast = toast;
|
||||
@@ -27,3 +35,17 @@ onMounted(() => {
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* Override styles for Unraid environment */
|
||||
[data-sonner-toast] [data-close-button] {
|
||||
min-width: inherit !important;
|
||||
}
|
||||
|
||||
/* Override Unraid webgui docker icon styles on sonner containers */
|
||||
[data-sonner-toast] [data-icon]:before,
|
||||
[data-sonner-toast] .fa-docker:before {
|
||||
font-family: inherit !important;
|
||||
content: '' !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createAjv } from '@jsonforms/core';
|
||||
import type Ajv from 'ajv';
|
||||
import type { Ajv } from 'ajv';
|
||||
import addErrors from 'ajv-errors';
|
||||
|
||||
export interface JsonFormsConfig {
|
||||
|
||||
@@ -31,6 +31,8 @@ export default function createConfig() {
|
||||
external: [
|
||||
'vue',
|
||||
'tailwindcss',
|
||||
'ajv',
|
||||
'ajv-errors',
|
||||
...(process.env.npm_lifecycle_script?.includes('storybook') ? [/^storybook\//] : []),
|
||||
],
|
||||
input: {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
For legacy compatibility, Unraid ships web components to the webgui. These components
|
||||
are written as Vue and turned into web components as a build step. By convention,
|
||||
Vue components that are built as top-level web components are suffixed with `*.ce.vue`
|
||||
Vue components that are built as top-level web components are suffixed with `*.standalone.vue`
|
||||
for "**c**ustom **e**lement", which comes from the tool used for compilation: `nuxt-custom-elements`.
|
||||
|
||||
Note: `nuxt-custom-elements` is currently pinned to a specific version because
|
||||
|
||||
@@ -9,7 +9,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { ComposerTranslation } from 'vue-i18n';
|
||||
|
||||
import WelcomeModal from '~/components/Activation/WelcomeModal.ce.vue';
|
||||
import WelcomeModal from '~/components/Activation/WelcomeModal.standalone.vue';
|
||||
|
||||
vi.mock('@unraid/ui', async (importOriginal) => {
|
||||
const actual = (await importOriginal()) as Record<string, unknown>;
|
||||
@@ -76,7 +76,7 @@ vi.mock('~/store/theme', () => ({
|
||||
useThemeStore: () => mockThemeStore,
|
||||
}));
|
||||
|
||||
describe('Activation/WelcomeModal.ce.vue', () => {
|
||||
describe('Activation/WelcomeModal.standalone.vue', () => {
|
||||
let mockSetProperty: ReturnType<typeof vi.fn>;
|
||||
let mockQuerySelector: ReturnType<typeof vi.fn>;
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { ServerconnectPluginInstalled } from '~/types/server';
|
||||
|
||||
import Auth from '~/components/Auth.ce.vue';
|
||||
import Auth from '~/components/Auth.standalone.vue';
|
||||
import { useServerStore } from '~/store/server';
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
|
||||
@@ -12,7 +12,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { MockInstance } from 'vitest';
|
||||
|
||||
import ColorSwitcher from '~/components/ColorSwitcher.ce.vue';
|
||||
import ColorSwitcher from '~/components/ColorSwitcher.standalone.vue';
|
||||
import { useThemeStore } from '~/store/theme';
|
||||
|
||||
// Explicitly mock @unraid/ui to ensure we use the actual components
|
||||
|
||||
@@ -8,7 +8,7 @@ import { mount } from '@vue/test-utils';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import DowngradeOs from '~/components/DowngradeOs.ce.vue';
|
||||
import DowngradeOs from '~/components/DowngradeOs.standalone.vue';
|
||||
import { useServerStore } from '~/store/server';
|
||||
|
||||
vi.mock('crypto-js/aes', () => ({
|
||||
|
||||
@@ -8,7 +8,7 @@ import { BrandButton } from '@unraid/ui';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import DownloadApiLogs from '~/components/DownloadApiLogs.ce.vue';
|
||||
import DownloadApiLogs from '~/components/DownloadApiLogs.standalone.vue';
|
||||
|
||||
vi.mock('~/helpers/urls', () => ({
|
||||
CONNECT_FORUMS: new URL('http://mock-forums.local'),
|
||||
|
||||
@@ -14,7 +14,7 @@ import type { VueWrapper } from '@vue/test-utils';
|
||||
import type { Error as CustomApiError } from '~/store/errors';
|
||||
import type { ServerUpdateOsResponse } from '~/types/server';
|
||||
|
||||
import HeaderOsVersion from '~/components/HeaderOsVersion.ce.vue';
|
||||
import HeaderOsVersion from '~/components/HeaderOsVersion.standalone.vue';
|
||||
import { useErrorsStore } from '~/store/errors';
|
||||
import { useServerStore } from '~/store/server';
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { VueWrapper } from '@vue/test-utils';
|
||||
import type { ServerconnectPluginInstalled } from '~/types/server';
|
||||
import type { Pinia } from 'pinia';
|
||||
|
||||
import Registration from '~/components/Registration.ce.vue';
|
||||
import Registration from '~/components/Registration.standalone.vue';
|
||||
import { usePurchaseStore } from '~/store/purchase';
|
||||
import { useReplaceRenewStore } from '~/store/replaceRenew';
|
||||
import { useServerStore } from '~/store/server';
|
||||
@@ -116,7 +116,7 @@ vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({ t }),
|
||||
}));
|
||||
|
||||
describe('Registration.ce.vue', () => {
|
||||
describe('Registration.standalone.vue', () => {
|
||||
let wrapper: VueWrapper<unknown>;
|
||||
let pinia: Pinia;
|
||||
let serverStore: ReturnType<typeof useServerStore>;
|
||||
|
||||
@@ -46,14 +46,14 @@ const mockLocation = {
|
||||
|
||||
vi.stubGlobal('location', mockLocation);
|
||||
|
||||
describe('ThemeSwitcher.ce.vue', () => {
|
||||
describe('ThemeSwitcher.standalone.vue', () => {
|
||||
let consoleDebugSpy: MockInstance;
|
||||
let consoleLogSpy: MockInstance;
|
||||
let consoleErrorSpy: MockInstance;
|
||||
let ThemeSwitcher: unknown;
|
||||
|
||||
beforeEach(async () => {
|
||||
ThemeSwitcher = (await import('~/components/ThemeSwitcher.ce.vue')).default;
|
||||
ThemeSwitcher = (await import('~/components/ThemeSwitcher.standalone.vue')).default;
|
||||
|
||||
vi.useFakeTimers();
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -8,7 +8,7 @@ import { mount } from '@vue/test-utils';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import UpdateOs from '~/components/UpdateOs.ce.vue';
|
||||
import UpdateOs from '~/components/UpdateOs.standalone.vue';
|
||||
|
||||
vi.mock('@unraid/ui', () => ({
|
||||
PageContainer: { template: '<div><slot /></div>' },
|
||||
@@ -61,7 +61,7 @@ const UpdateOsThirdPartyDriversStub = {
|
||||
props: ['t'],
|
||||
};
|
||||
|
||||
describe('UpdateOs.ce.vue', () => {
|
||||
describe('UpdateOs.standalone.vue', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockRebootType.value = '';
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { VueWrapper } from '@vue/test-utils';
|
||||
import type { Server, ServerconnectPluginInstalled, ServerState } from '~/types/server';
|
||||
import type { Pinia } from 'pinia';
|
||||
|
||||
import UserProfile from '~/components/UserProfile.ce.vue';
|
||||
import UserProfile from '~/components/UserProfile.standalone.vue';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import { useThemeStore } from '~/store/theme';
|
||||
|
||||
@@ -101,7 +101,7 @@ const stubs = {
|
||||
UpcDropdownTrigger: { template: '<button data-testid="dropdown-trigger"></button>' },
|
||||
};
|
||||
|
||||
describe('UserProfile.ce.vue', () => {
|
||||
describe('UserProfile.standalone.vue', () => {
|
||||
let wrapper: VueWrapper<InstanceType<typeof UserProfile>>;
|
||||
let pinia: Pinia;
|
||||
let serverStore: ReturnType<typeof useServerStore>;
|
||||
|
||||
@@ -93,7 +93,7 @@ const WanIpCheckStub = defineComponent({
|
||||
},
|
||||
});
|
||||
|
||||
describe('WanIpCheck.ce.vue', () => {
|
||||
describe('WanIpCheck.standalone.vue', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ function calculateTitle(partnerName: string | null) {
|
||||
: 'Welcome to Unraid!';
|
||||
}
|
||||
|
||||
describe('WelcomeModal.ce.vue', () => {
|
||||
describe('WelcomeModal.standalone.vue', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockPartnerName.mockReturnValue(null);
|
||||
|
||||
@@ -1,55 +1,55 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
// Mock all the component imports
|
||||
vi.mock('~/components/Auth.ce.vue', () => ({
|
||||
vi.mock('~/components/Auth.standalone.vue', () => ({
|
||||
default: { name: 'MockAuth', template: '<div>Auth</div>' },
|
||||
}));
|
||||
vi.mock('~/components/ConnectSettings/ConnectSettings.ce.vue', () => ({
|
||||
vi.mock('~/components/ConnectSettings/ConnectSettings.standalone.vue', () => ({
|
||||
default: { name: 'MockConnectSettings', template: '<div>ConnectSettings</div>' },
|
||||
}));
|
||||
vi.mock('~/components/DownloadApiLogs.ce.vue', () => ({
|
||||
vi.mock('~/components/DownloadApiLogs.standalone.vue', () => ({
|
||||
default: { name: 'MockDownloadApiLogs', template: '<div>DownloadApiLogs</div>' },
|
||||
}));
|
||||
vi.mock('~/components/HeaderOsVersion.ce.vue', () => ({
|
||||
vi.mock('~/components/HeaderOsVersion.standalone.vue', () => ({
|
||||
default: { name: 'MockHeaderOsVersion', template: '<div>HeaderOsVersion</div>' },
|
||||
}));
|
||||
vi.mock('~/components/Modals.ce.vue', () => ({
|
||||
vi.mock('~/components/Modals.standalone.vue', () => ({
|
||||
default: { name: 'MockModals', template: '<div>Modals</div>' },
|
||||
}));
|
||||
vi.mock('~/components/UserProfile.ce.vue', () => ({
|
||||
vi.mock('~/components/UserProfile.standalone.vue', () => ({
|
||||
default: { name: 'MockUserProfile', template: '<div>UserProfile</div>' },
|
||||
}));
|
||||
vi.mock('~/components/UpdateOs.ce.vue', () => ({
|
||||
vi.mock('~/components/UpdateOs.standalone.vue', () => ({
|
||||
default: { name: 'MockUpdateOs', template: '<div>UpdateOs</div>' },
|
||||
}));
|
||||
vi.mock('~/components/DowngradeOs.ce.vue', () => ({
|
||||
vi.mock('~/components/DowngradeOs.standalone.vue', () => ({
|
||||
default: { name: 'MockDowngradeOs', template: '<div>DowngradeOs</div>' },
|
||||
}));
|
||||
vi.mock('~/components/Registration.ce.vue', () => ({
|
||||
vi.mock('~/components/Registration.standalone.vue', () => ({
|
||||
default: { name: 'MockRegistration', template: '<div>Registration</div>' },
|
||||
}));
|
||||
vi.mock('~/components/WanIpCheck.ce.vue', () => ({
|
||||
vi.mock('~/components/WanIpCheck.standalone.vue', () => ({
|
||||
default: { name: 'MockWanIpCheck', template: '<div>WanIpCheck</div>' },
|
||||
}));
|
||||
vi.mock('~/components/Activation/WelcomeModal.ce.vue', () => ({
|
||||
vi.mock('~/components/Activation/WelcomeModal.standalone.vue', () => ({
|
||||
default: { name: 'MockWelcomeModal', template: '<div>WelcomeModal</div>' },
|
||||
}));
|
||||
vi.mock('~/components/SsoButton.ce.vue', () => ({
|
||||
vi.mock('~/components/SsoButton.standalone.vue', () => ({
|
||||
default: { name: 'MockSsoButton', template: '<div>SsoButton</div>' },
|
||||
}));
|
||||
vi.mock('~/components/Logs/LogViewer.ce.vue', () => ({
|
||||
vi.mock('~/components/Logs/LogViewer.standalone.vue', () => ({
|
||||
default: { name: 'MockLogViewer', template: '<div>LogViewer</div>' },
|
||||
}));
|
||||
vi.mock('~/components/ThemeSwitcher.ce.vue', () => ({
|
||||
vi.mock('~/components/ThemeSwitcher.standalone.vue', () => ({
|
||||
default: { name: 'MockThemeSwitcher', template: '<div>ThemeSwitcher</div>' },
|
||||
}));
|
||||
vi.mock('~/components/ApiKeyPage.ce.vue', () => ({
|
||||
vi.mock('~/components/ApiKeyPage.standalone.vue', () => ({
|
||||
default: { name: 'MockApiKeyPage', template: '<div>ApiKeyPage</div>' },
|
||||
}));
|
||||
vi.mock('~/components/DevModalTest.ce.vue', () => ({
|
||||
vi.mock('~/components/DevModalTest.standalone.vue', () => ({
|
||||
default: { name: 'MockDevModalTest', template: '<div>DevModalTest</div>' },
|
||||
}));
|
||||
vi.mock('~/components/ApiKeyAuthorize.ce.vue', () => ({
|
||||
vi.mock('~/components/ApiKeyAuthorize.standalone.vue', () => ({
|
||||
default: { name: 'MockApiKeyAuthorize', template: '<div>ApiKeyAuthorize</div>' },
|
||||
}));
|
||||
vi.mock('~/components/UnraidToaster.vue', () => ({
|
||||
|
||||
86
web/auto-imports.d.ts
vendored
86
web/auto-imports.d.ts
vendored
@@ -6,57 +6,57 @@
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
const avatarGroupInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useAvatarGroup.js')['avatarGroupInjectionKey']
|
||||
const defineLocale: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/defineLocale.js')['defineLocale']
|
||||
const defineShortcuts: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.js')['defineShortcuts']
|
||||
const extendLocale: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/defineLocale.js')['extendLocale']
|
||||
const extractShortcuts: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.js')['extractShortcuts']
|
||||
const fieldGroupInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFieldGroup.js')['fieldGroupInjectionKey']
|
||||
const formBusInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formBusInjectionKey']
|
||||
const formFieldInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formFieldInjectionKey']
|
||||
const formInputsInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formInputsInjectionKey']
|
||||
const formLoadingInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formLoadingInjectionKey']
|
||||
const formOptionsInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formOptionsInjectionKey']
|
||||
const inputIdInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['inputIdInjectionKey']
|
||||
const kbdKeysMap: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.js')['kbdKeysMap']
|
||||
const localeContextInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useLocale.js')['localeContextInjectionKey']
|
||||
const portalTargetInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/usePortal.js')['portalTargetInjectionKey']
|
||||
const useAppConfig: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/vue/composables/useAppConfig.js')['useAppConfig']
|
||||
const useAvatarGroup: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useAvatarGroup.js')['useAvatarGroup']
|
||||
const useComponentIcons: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useComponentIcons.js')['useComponentIcons']
|
||||
const useContentSearch: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useContentSearch.js')['useContentSearch']
|
||||
const useFieldGroup: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFieldGroup.js')['useFieldGroup']
|
||||
const useFileUpload: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFileUpload.js')['useFileUpload']
|
||||
const useFormField: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['useFormField']
|
||||
const useKbd: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.js')['useKbd']
|
||||
const useLocale: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useLocale.js')['useLocale']
|
||||
const useOverlay: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useOverlay.js')['useOverlay']
|
||||
const usePortal: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/usePortal.js')['usePortal']
|
||||
const useResizable: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useResizable.js')['useResizable']
|
||||
const useScrollspy: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useScrollspy.js')['useScrollspy']
|
||||
const useToast: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useToast.js')['useToast']
|
||||
const avatarGroupInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useAvatarGroup.js')['avatarGroupInjectionKey']
|
||||
const defineLocale: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineLocale.js')['defineLocale']
|
||||
const defineShortcuts: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.js')['defineShortcuts']
|
||||
const extendLocale: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineLocale.js')['extendLocale']
|
||||
const extractShortcuts: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.js')['extractShortcuts']
|
||||
const fieldGroupInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFieldGroup.js')['fieldGroupInjectionKey']
|
||||
const formBusInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formBusInjectionKey']
|
||||
const formFieldInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formFieldInjectionKey']
|
||||
const formInputsInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formInputsInjectionKey']
|
||||
const formLoadingInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formLoadingInjectionKey']
|
||||
const formOptionsInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formOptionsInjectionKey']
|
||||
const inputIdInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['inputIdInjectionKey']
|
||||
const kbdKeysMap: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.js')['kbdKeysMap']
|
||||
const localeContextInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useLocale.js')['localeContextInjectionKey']
|
||||
const portalTargetInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/usePortal.js')['portalTargetInjectionKey']
|
||||
const useAppConfig: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/vue/composables/useAppConfig.js')['useAppConfig']
|
||||
const useAvatarGroup: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useAvatarGroup.js')['useAvatarGroup']
|
||||
const useComponentIcons: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useComponentIcons.js')['useComponentIcons']
|
||||
const useContentSearch: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useContentSearch.js')['useContentSearch']
|
||||
const useFieldGroup: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFieldGroup.js')['useFieldGroup']
|
||||
const useFileUpload: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFileUpload.js')['useFileUpload']
|
||||
const useFormField: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['useFormField']
|
||||
const useKbd: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.js')['useKbd']
|
||||
const useLocale: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useLocale.js')['useLocale']
|
||||
const useOverlay: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useOverlay.js')['useOverlay']
|
||||
const usePortal: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/usePortal.js')['usePortal']
|
||||
const useResizable: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useResizable.js')['useResizable']
|
||||
const useScrollspy: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useScrollspy.js')['useScrollspy']
|
||||
const useToast: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useToast.js')['useToast']
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { ShortcutConfig, ShortcutsConfig, ShortcutsOptions } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.d'
|
||||
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.d')
|
||||
export type { ShortcutConfig, ShortcutsConfig, ShortcutsOptions } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.d'
|
||||
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.d')
|
||||
// @ts-ignore
|
||||
export type { UseComponentIconsProps } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useComponentIcons.d'
|
||||
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useComponentIcons.d')
|
||||
export type { UseComponentIconsProps } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useComponentIcons.d'
|
||||
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useComponentIcons.d')
|
||||
// @ts-ignore
|
||||
export type { UseFileUploadOptions } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFileUpload.d'
|
||||
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFileUpload.d')
|
||||
export type { UseFileUploadOptions } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFileUpload.d'
|
||||
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFileUpload.d')
|
||||
// @ts-ignore
|
||||
export type { KbdKey, KbdKeySpecific } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.d'
|
||||
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.d')
|
||||
export type { KbdKey, KbdKeySpecific } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.d'
|
||||
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.d')
|
||||
// @ts-ignore
|
||||
export type { OverlayOptions, Overlay } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useOverlay.d'
|
||||
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useOverlay.d')
|
||||
export type { OverlayOptions, Overlay } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useOverlay.d'
|
||||
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useOverlay.d')
|
||||
// @ts-ignore
|
||||
export type { UseResizableProps, UseResizableReturn } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useResizable.d'
|
||||
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useResizable.d')
|
||||
export type { UseResizableProps, UseResizableReturn } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useResizable.d'
|
||||
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useResizable.d')
|
||||
// @ts-ignore
|
||||
export type { Toast } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useToast.d'
|
||||
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useToast.d')
|
||||
export type { Toast } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useToast.d'
|
||||
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useToast.d')
|
||||
}
|
||||
|
||||
70
web/components.d.ts
vendored
70
web/components.d.ts
vendored
@@ -12,17 +12,17 @@ declare module 'vue' {
|
||||
ActivationPartnerLogo: typeof import('./src/components/Activation/ActivationPartnerLogo.vue')['default']
|
||||
ActivationPartnerLogoImg: typeof import('./src/components/Activation/ActivationPartnerLogoImg.vue')['default']
|
||||
ActivationSteps: typeof import('./src/components/Activation/ActivationSteps.vue')['default']
|
||||
'ApiKeyAuthorize.ce': typeof import('./src/components/ApiKeyAuthorize.ce.vue')['default']
|
||||
'ApiKeyAuthorize.standalone': typeof import('./src/components/ApiKeyAuthorize.standalone.vue')['default']
|
||||
ApiKeyCreate: typeof import('./src/components/ApiKey/ApiKeyCreate.vue')['default']
|
||||
ApiKeyManager: typeof import('./src/components/ApiKey/ApiKeyManager.vue')['default']
|
||||
'ApiKeyPage.ce': typeof import('./src/components/ApiKeyPage.ce.vue')['default']
|
||||
'Auth.ce': typeof import('./src/components/Auth.ce.vue')['default']
|
||||
'ApiKeyPage.standalone': typeof import('./src/components/ApiKeyPage.standalone.vue')['default']
|
||||
'Auth.standalone': typeof import('./src/components/Auth.standalone.vue')['default']
|
||||
Avatar: typeof import('./src/components/Brand/Avatar.vue')['default']
|
||||
Beta: typeof import('./src/components/UserProfile/Beta.vue')['default']
|
||||
CallbackButton: typeof import('./src/components/UpdateOs/CallbackButton.vue')['default']
|
||||
CallbackFeedback: typeof import('./src/components/UserProfile/CallbackFeedback.vue')['default']
|
||||
CallbackFeedbackStatus: typeof import('./src/components/UserProfile/CallbackFeedbackStatus.vue')['default']
|
||||
'CallbackHandler.ce': typeof import('./src/components/CallbackHandler.ce.vue')['default']
|
||||
'CallbackHandler.standalone': typeof import('./src/components/CallbackHandler.standalone.vue')['default']
|
||||
Card: typeof import('./src/components/LayoutViews/Card/Card.vue')['default']
|
||||
CardGrid: typeof import('./src/components/LayoutViews/Card/CardGrid.vue')['default']
|
||||
CardGroupHeader: typeof import('./src/components/LayoutViews/Card/CardGroupHeader.vue')['default']
|
||||
@@ -30,20 +30,22 @@ declare module 'vue' {
|
||||
CardItem: typeof import('./src/components/LayoutViews/Card/CardItem.vue')['default']
|
||||
ChangelogModal: typeof import('./src/components/UpdateOs/ChangelogModal.vue')['default']
|
||||
CheckUpdateResponseModal: typeof import('./src/components/UpdateOs/CheckUpdateResponseModal.vue')['default']
|
||||
'ColorSwitcher.ce': typeof import('./src/components/ColorSwitcher.ce.vue')['default']
|
||||
'ConnectSettings.ce': typeof import('./src/components/ConnectSettings/ConnectSettings.ce.vue')['default']
|
||||
'ColorSwitcher.standalone': typeof import('./src/components/ColorSwitcher.standalone.vue')['default']
|
||||
ConfirmDialog: typeof import('./src/components/ConfirmDialog.vue')['default']
|
||||
'ConnectSettings.standalone': typeof import('./src/components/ConnectSettings/ConnectSettings.standalone.vue')['default']
|
||||
Console: typeof import('./src/components/Docker/Console.vue')['default']
|
||||
'CpuStats.standalone': typeof import('./src/components/CpuStats/CpuStats.standalone.vue')['default']
|
||||
Detail: typeof import('./src/components/LayoutViews/Detail/Detail.vue')['default']
|
||||
DetailContentHeader: typeof import('./src/components/LayoutViews/Detail/DetailContentHeader.vue')['default']
|
||||
DetailLeftNavigation: typeof import('./src/components/LayoutViews/Detail/DetailLeftNavigation.vue')['default']
|
||||
DetailRightContent: typeof import('./src/components/LayoutViews/Detail/DetailRightContent.vue')['default']
|
||||
'DetailTest.ce': typeof import('./src/components/LayoutViews/Detail/DetailTest.ce.vue')['default']
|
||||
'DetailTest.standalone': typeof import('./src/components/LayoutViews/Detail/DetailTest.standalone.vue')['default']
|
||||
DeveloperAuthorizationLink: typeof import('./src/components/ApiKey/DeveloperAuthorizationLink.vue')['default']
|
||||
'DevModalTest.ce': typeof import('./src/components/DevModalTest.ce.vue')['default']
|
||||
'DevModalTest.standalone': typeof import('./src/components/DevModalTest.standalone.vue')['default']
|
||||
DevSettings: typeof import('./src/components/DevSettings.vue')['default']
|
||||
Downgrade: typeof import('./src/components/UpdateOs/Downgrade.vue')['default']
|
||||
'DowngradeOs.ce': typeof import('./src/components/DowngradeOs.ce.vue')['default']
|
||||
'DownloadApiLogs.ce': typeof import('./src/components/DownloadApiLogs.ce.vue')['default']
|
||||
'DowngradeOs.standalone': typeof import('./src/components/DowngradeOs.standalone.vue')['default']
|
||||
'DownloadApiLogs.standalone': typeof import('./src/components/DownloadApiLogs.standalone.vue')['default']
|
||||
DropdownConnectStatus: typeof import('./src/components/UserProfile/DropdownConnectStatus.vue')['default']
|
||||
DropdownContent: typeof import('./src/components/UserProfile/DropdownContent.vue')['default']
|
||||
DropdownError: typeof import('./src/components/UserProfile/DropdownError.vue')['default']
|
||||
@@ -57,7 +59,7 @@ declare module 'vue' {
|
||||
FileViewer: typeof import('./src/components/FileViewer.vue')['default']
|
||||
FilteredLogModal: typeof import('./src/components/Logs/FilteredLogModal.vue')['default']
|
||||
HeaderContent: typeof import('./src/components/Docker/HeaderContent.vue')['default']
|
||||
'HeaderOsVersion.ce': typeof import('./src/components/HeaderOsVersion.ce.vue')['default']
|
||||
'HeaderOsVersion.standalone': typeof import('./src/components/HeaderOsVersion.standalone.vue')['default']
|
||||
IgnoredRelease: typeof import('./src/components/UpdateOs/IgnoredRelease.vue')['default']
|
||||
Indicator: typeof import('./src/components/Notifications/Indicator.vue')['default']
|
||||
Item: typeof import('./src/components/Notifications/Item.vue')['default']
|
||||
@@ -68,11 +70,11 @@ declare module 'vue' {
|
||||
LogFilterInput: typeof import('./src/components/Logs/LogFilterInput.vue')['default']
|
||||
Logo: typeof import('./src/components/Brand/Logo.vue')['default']
|
||||
Logs: typeof import('./src/components/Docker/Logs.vue')['default']
|
||||
'LogViewer.ce': typeof import('./src/components/Logs/LogViewer.ce.vue')['default']
|
||||
'LogViewer.standalone': typeof import('./src/components/Logs/LogViewer.standalone.vue')['default']
|
||||
LogViewerToolbar: typeof import('./src/components/Logs/LogViewerToolbar.vue')['default']
|
||||
Mark: typeof import('./src/components/Brand/Mark.vue')['default']
|
||||
Modal: typeof import('./src/components/Modal.vue')['default']
|
||||
'Modals.ce': typeof import('./src/components/Modals.ce.vue')['default']
|
||||
'Modals.standalone': typeof import('./src/components/Modals.standalone.vue')['default']
|
||||
OidcDebugButton: typeof import('./src/components/Logs/OidcDebugButton.vue')['default']
|
||||
OidcDebugLogs: typeof import('./src/components/ConnectSettings/OidcDebugLogs.vue')['default']
|
||||
Overview: typeof import('./src/components/Docker/Overview.vue')['default']
|
||||
@@ -81,7 +83,7 @@ declare module 'vue' {
|
||||
RawChangelogRenderer: typeof import('./src/components/UpdateOs/RawChangelogRenderer.vue')['default']
|
||||
RCloneConfig: typeof import('./src/components/RClone/RCloneConfig.vue')['default']
|
||||
RCloneOverview: typeof import('./src/components/RClone/RCloneOverview.vue')['default']
|
||||
'Registration.ce': typeof import('./src/components/Registration.ce.vue')['default']
|
||||
'Registration.standalone': typeof import('./src/components/Registration.standalone.vue')['default']
|
||||
ReleaseNotesModal: typeof import('./src/components/ReleaseNotesModal.vue')['default']
|
||||
RemoteItem: typeof import('./src/components/RClone/RemoteItem.vue')['default']
|
||||
ReplaceCheck: typeof import('./src/components/Registration/ReplaceCheck.vue')['default']
|
||||
@@ -93,35 +95,37 @@ declare module 'vue' {
|
||||
ServerStatus: typeof import('./src/components/UserProfile/ServerStatus.vue')['default']
|
||||
Sidebar: typeof import('./src/components/Notifications/Sidebar.vue')['default']
|
||||
SingleLogViewer: typeof import('./src/components/Logs/SingleLogViewer.vue')['default']
|
||||
'SsoButton.ce': typeof import('./src/components/SsoButton.ce.vue')['default']
|
||||
'SsoButton.standalone': typeof import('./src/components/SsoButton.standalone.vue')['default']
|
||||
SsoButtons: typeof import('./src/components/sso/SsoButtons.vue')['default']
|
||||
SsoProviderButton: typeof import('./src/components/sso/SsoProviderButton.vue')['default']
|
||||
Status: typeof import('./src/components/UpdateOs/Status.vue')['default']
|
||||
'ThemeSwitcher.ce': typeof import('./src/components/ThemeSwitcher.ce.vue')['default']
|
||||
'TestThemeSwitcher.standalone': typeof import('./src/components/TestThemeSwitcher.standalone.vue')['default']
|
||||
'TestUpdateModal.standalone': typeof import('./src/components/UpdateOs/TestUpdateModal.standalone.vue')['default']
|
||||
'ThemeSwitcher.standalone': typeof import('./src/components/ThemeSwitcher.standalone.vue')['default']
|
||||
ThirdPartyDrivers: typeof import('./src/components/UpdateOs/ThirdPartyDrivers.vue')['default']
|
||||
Trial: typeof import('./src/components/UserProfile/Trial.vue')['default']
|
||||
UBadge: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/Badge.vue')['default']
|
||||
UButton: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/Button.vue')['default']
|
||||
UCard: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/Card.vue')['default']
|
||||
UCheckbox: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/Checkbox.vue')['default']
|
||||
UDrawer: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/Drawer.vue')['default']
|
||||
UDropdownMenu: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/DropdownMenu.vue')['default']
|
||||
UFormField: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/FormField.vue')['default']
|
||||
UIcon: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/vue/components/Icon.vue')['default']
|
||||
UInput: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/Input.vue')['default']
|
||||
UNavigationMenu: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/NavigationMenu.vue')['default']
|
||||
UBadge: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/Badge.vue')['default']
|
||||
UButton: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/Button.vue')['default']
|
||||
UCard: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/Card.vue')['default']
|
||||
UCheckbox: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/Checkbox.vue')['default']
|
||||
UDrawer: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/Drawer.vue')['default']
|
||||
UDropdownMenu: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/DropdownMenu.vue')['default']
|
||||
UFormField: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/FormField.vue')['default']
|
||||
UIcon: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/vue/components/Icon.vue')['default']
|
||||
UInput: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/Input.vue')['default']
|
||||
UNavigationMenu: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/NavigationMenu.vue')['default']
|
||||
UnraidToaster: typeof import('./src/components/UnraidToaster.vue')['default']
|
||||
Update: typeof import('./src/components/UpdateOs/Update.vue')['default']
|
||||
UpdateExpiration: typeof import('./src/components/Registration/UpdateExpiration.vue')['default']
|
||||
UpdateExpirationAction: typeof import('./src/components/Registration/UpdateExpirationAction.vue')['default']
|
||||
UpdateIneligible: typeof import('./src/components/UpdateOs/UpdateIneligible.vue')['default']
|
||||
'UpdateOs.ce': typeof import('./src/components/UpdateOs.ce.vue')['default']
|
||||
'UpdateOs.standalone': typeof import('./src/components/UpdateOs.standalone.vue')['default']
|
||||
UptimeExpire: typeof import('./src/components/UserProfile/UptimeExpire.vue')['default']
|
||||
USelectMenu: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/SelectMenu.vue')['default']
|
||||
'UserProfile.ce': typeof import('./src/components/UserProfile.ce.vue')['default']
|
||||
USwitch: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/Switch.vue')['default']
|
||||
UTabs: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/Tabs.vue')['default']
|
||||
'WanIpCheck.ce': typeof import('./src/components/WanIpCheck.ce.vue')['default']
|
||||
'WelcomeModal.ce': typeof import('./src/components/Activation/WelcomeModal.ce.vue')['default']
|
||||
USelectMenu: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/SelectMenu.vue')['default']
|
||||
'UserProfile.standalone': typeof import('./src/components/UserProfile.standalone.vue')['default']
|
||||
USwitch: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/Switch.vue')['default']
|
||||
UTabs: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/Tabs.vue')['default']
|
||||
'WanIpCheck.standalone': typeof import('./src/components/WanIpCheck.standalone.vue')['default']
|
||||
'WelcomeModal.standalone': typeof import('./src/components/Activation/WelcomeModal.standalone.vue')['default']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@unraid/web",
|
||||
"version": "4.21.0",
|
||||
"version": "4.22.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "GPL-2.0-or-later",
|
||||
@@ -105,6 +105,7 @@
|
||||
"@vueuse/integrations": "13.8.0",
|
||||
"ajv": "8.17.1",
|
||||
"ansi_up": "6.0.6",
|
||||
"chart.js": "^4.5.0",
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
"crypto-js": "4.2.0",
|
||||
@@ -123,6 +124,7 @@
|
||||
"postcss-import": "16.1.1",
|
||||
"semver": "7.7.2",
|
||||
"tailwind-merge": "2.6.0",
|
||||
"vue-chartjs": "^5.3.2",
|
||||
"vue-i18n": "11.1.11",
|
||||
"vue-router": "4.5.1",
|
||||
"vue-web-component-wrapper": "1.7.7",
|
||||
|
||||
@@ -103,6 +103,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>CPU Statistics</h2>
|
||||
<div class="component-mount" data-component="unraid-cpu-stats">
|
||||
<unraid-cpu-stats></unraid-cpu-stats>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Theme Settings</h2>
|
||||
<div class="component-mount" data-component="unraid-theme-switcher">
|
||||
|
||||
@@ -179,6 +179,14 @@
|
||||
</div>
|
||||
<a href="/test-pages/os-management.html">Open →</a>
|
||||
</div>
|
||||
|
||||
<div class="page-item">
|
||||
<div>
|
||||
<h3>Update Modal Testing <span class="badge new">NEW</span></h3>
|
||||
<p>Test various update scenarios including expired licenses, renewals, and auth requirements</p>
|
||||
</div>
|
||||
<a href="/test-update-modal.html">Open →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -146,9 +146,5 @@
|
||||
/* Style for Unraid progress frame */
|
||||
iframe#progressFrame {
|
||||
background-color: var(--background-color);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
/* Global input text color when SSO button is present (for login page) */
|
||||
body:has(unraid-sso-button) input {
|
||||
color: #1b1b1b !important;
|
||||
}
|
||||
38
web/src/components/ConfirmDialog.vue
Normal file
38
web/src/components/ConfirmDialog.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Button,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogRoot,
|
||||
DialogTitle,
|
||||
} from '@unraid/ui';
|
||||
|
||||
import { useConfirm } from '~/composables/useConfirm';
|
||||
|
||||
const { isOpen, state, handleConfirm, handleCancel } = useConfirm();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogRoot :open="isOpen" @update:open="!$event && handleCancel()">
|
||||
<DialogContent>
|
||||
<DialogHeader v-if="state">
|
||||
<DialogTitle>{{ state.title }}</DialogTitle>
|
||||
<DialogDescription v-if="state.description">
|
||||
{{ state.description }}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter v-if="state">
|
||||
<div class="flex w-full justify-between gap-3">
|
||||
<Button variant="outline" @click="handleCancel">
|
||||
{{ state.cancelText }}
|
||||
</Button>
|
||||
<Button :variant="state.confirmVariant" @click="handleConfirm">
|
||||
{{ state.confirmText }}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</DialogRoot>
|
||||
</template>
|
||||
@@ -11,7 +11,7 @@ import { watchDebounced } from '@vueuse/core';
|
||||
import { BrandButton, jsonFormsAjv, jsonFormsRenderers, Label, SettingsGrid } from '@unraid/ui';
|
||||
import { JsonForms } from '@jsonforms/vue';
|
||||
|
||||
import Auth from '~/components/Auth.ce.vue';
|
||||
import Auth from '~/components/Auth.standalone.vue';
|
||||
// unified settings values are returned as JSON, so use a generic record type
|
||||
// import type { ConnectSettingsValues } from '~/composables/gql/graphql';
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
updateConnectSettings,
|
||||
} from '~/components/ConnectSettings/graphql/settings.query';
|
||||
import OidcDebugLogs from '~/components/ConnectSettings/OidcDebugLogs.vue';
|
||||
import DownloadApiLogs from '~/components/DownloadApiLogs.ce.vue';
|
||||
import DownloadApiLogs from '~/components/DownloadApiLogs.standalone.vue';
|
||||
import { useServerStore } from '~/store/server';
|
||||
|
||||
// Disable automatic attribute inheritance
|
||||
308
web/src/components/CpuStats/CpuStats.standalone.vue
Normal file
308
web/src/components/CpuStats/CpuStats.standalone.vue
Normal file
@@ -0,0 +1,308 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, shallowRef, watch } from 'vue';
|
||||
import { useQuery, useSubscription } from '@vue/apollo-composable';
|
||||
import { GET_CPU_INFO, CPU_METRICS_SUBSCRIPTION } from './cpu-stats.query';
|
||||
import { Line } from 'vue-chartjs';
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler,
|
||||
type ChartOptions,
|
||||
type ChartData
|
||||
} from 'chart.js';
|
||||
import { Button, Select } from '@unraid/ui';
|
||||
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler
|
||||
);
|
||||
|
||||
const showDetails = ref(true);
|
||||
const cpuHistory = ref<number[]>([]);
|
||||
|
||||
// History duration options
|
||||
type HistoryDuration = '10s' | '30s' | '1m' | '2m' | '5m';
|
||||
|
||||
const historyDuration = ref<HistoryDuration>('30s');
|
||||
|
||||
const historyConfigs: Record<HistoryDuration, { points: number; interval: number }> = {
|
||||
'10s': { points: 60, interval: 167 }, // ~6 fps
|
||||
'30s': { points: 60, interval: 500 }, // 2 fps
|
||||
'1m': { points: 60, interval: 1000 }, // 1 fps
|
||||
'2m': { points: 60, interval: 2000 }, // 0.5 fps
|
||||
'5m': { points: 60, interval: 5000 }, // 0.2 fps
|
||||
};
|
||||
|
||||
const historyOptions = [
|
||||
{ value: '10s', label: '10 seconds' },
|
||||
{ value: '30s', label: '30 seconds' },
|
||||
{ value: '1m', label: '1 minute' },
|
||||
{ value: '2m', label: '2 minutes' },
|
||||
{ value: '5m', label: '5 minutes' },
|
||||
];
|
||||
|
||||
const currentHistoryConfig = computed(() =>
|
||||
historyConfigs[historyDuration.value] || historyConfigs['30s']
|
||||
);
|
||||
|
||||
const { result: cpuInfoResult } = useQuery(GET_CPU_INFO);
|
||||
const { result: cpuMetricsResult } = useSubscription(CPU_METRICS_SUBSCRIPTION);
|
||||
|
||||
const cpuInfo = computed(() => cpuInfoResult.value?.info?.cpu);
|
||||
const cpuMetrics = computed(() => cpuMetricsResult.value?.systemMetricsCpu);
|
||||
|
||||
const cpuBrand = computed(() => {
|
||||
if (!cpuInfo.value) return 'Loading...';
|
||||
const brand = cpuInfo.value.brand || cpuInfo.value.model || 'Unknown CPU';
|
||||
return brand;
|
||||
});
|
||||
|
||||
const overallLoad = computed(() => {
|
||||
if (!cpuMetrics.value) return 0;
|
||||
return Math.floor(cpuMetrics.value.percentTotal);
|
||||
});
|
||||
|
||||
const cpuCores = computed(() => {
|
||||
if (!cpuMetrics.value?.cpus) return [];
|
||||
return cpuMetrics.value.cpus.map((cpu, index) => ({
|
||||
index: index * 2, // Assuming HT, so multiply by 2
|
||||
htIndex: index * 2 + 1,
|
||||
percent: Math.floor(cpu.percentTotal),
|
||||
percentUser: Math.floor(cpu.percentUser),
|
||||
percentSystem: Math.floor(cpu.percentSystem),
|
||||
}));
|
||||
});
|
||||
|
||||
// Keep chart data simple - just the last 60 data points
|
||||
const chartDataRef = shallowRef<ChartData<'line'>>({
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: 'CPU Usage %',
|
||||
data: [],
|
||||
borderColor: '#ff8c2e',
|
||||
backgroundColor: 'rgba(255, 140, 46, 0.1)',
|
||||
borderWidth: 2,
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 3,
|
||||
}]
|
||||
});
|
||||
|
||||
// Update chart data without triggering computed re-evaluation
|
||||
const updateChartData = () => {
|
||||
// Create simple numeric labels (no timestamps, just indices)
|
||||
const labels = Array.from({ length: cpuHistory.value.length }, (_, i) => '');
|
||||
|
||||
// Update the ref value directly
|
||||
chartDataRef.value = {
|
||||
labels,
|
||||
datasets: [{
|
||||
label: 'CPU Usage %',
|
||||
data: [...cpuHistory.value], // Clone to prevent reactivity issues
|
||||
borderColor: '#ff8c2e',
|
||||
backgroundColor: 'rgba(255, 140, 46, 0.1)',
|
||||
borderWidth: 2,
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 0, // Disable hover points for performance
|
||||
}]
|
||||
};
|
||||
};
|
||||
|
||||
const chartData = computed(() => chartDataRef.value);
|
||||
|
||||
const chartOptions = computed<ChartOptions<'line'>>(() => ({
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: {
|
||||
duration: 0 // Disable all animations for performance
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false // Disable tooltips for performance
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
display: false
|
||||
},
|
||||
display: false // Hide x-axis completely for performance
|
||||
},
|
||||
y: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
grid: {
|
||||
color: 'rgb(229, 231, 235)'
|
||||
},
|
||||
ticks: {
|
||||
stepSize: 25,
|
||||
color: 'rgb(107, 114, 128)',
|
||||
font: {
|
||||
size: 11
|
||||
},
|
||||
callback: (value) => `${value}%`
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
let updateInterval: NodeJS.Timeout | null = null;
|
||||
let tickInterval: NodeJS.Timeout | null = null;
|
||||
let lastKnownValue = 0;
|
||||
|
||||
// Update with actual data from subscription
|
||||
const updateFromMetrics = () => {
|
||||
if (cpuMetrics.value) {
|
||||
lastKnownValue = Math.floor(cpuMetrics.value.percentTotal);
|
||||
}
|
||||
};
|
||||
|
||||
// Tick the chart forward with the last known value
|
||||
const tick = () => {
|
||||
// Always push a value (either new or repeated last known)
|
||||
cpuHistory.value.push(lastKnownValue);
|
||||
|
||||
// Keep only the configured number of data points
|
||||
if (cpuHistory.value.length > currentHistoryConfig.value.points) {
|
||||
cpuHistory.value.shift();
|
||||
}
|
||||
|
||||
// Update chart data
|
||||
updateChartData();
|
||||
};
|
||||
|
||||
// Watch for actual metric changes
|
||||
watch(cpuMetrics, updateFromMetrics, { immediate: true });
|
||||
|
||||
// Restart ticker when duration changes
|
||||
const restartTicker = () => {
|
||||
if (tickInterval) {
|
||||
clearInterval(tickInterval);
|
||||
}
|
||||
|
||||
// Clear history when changing duration for clean transition
|
||||
cpuHistory.value = [];
|
||||
|
||||
// Start new ticker with appropriate interval
|
||||
tickInterval = setInterval(tick, currentHistoryConfig.value.interval);
|
||||
};
|
||||
|
||||
watch(historyDuration, restartTicker);
|
||||
|
||||
onMounted(() => {
|
||||
// Start ticker with initial interval
|
||||
tickInterval = setInterval(tick, currentHistoryConfig.value.interval);
|
||||
tick(); // Initial tick
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (updateInterval) {
|
||||
clearInterval(updateInterval);
|
||||
}
|
||||
if (tickInterval) {
|
||||
clearInterval(tickInterval);
|
||||
}
|
||||
});
|
||||
|
||||
const toggleDetails = () => {
|
||||
showDetails.value = !showDetails.value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-background rounded-md border-2 border-muted shadow-md p-4">
|
||||
<div class="space-y-4">
|
||||
<!-- Header Section -->
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-foreground">Processor</h3>
|
||||
<div class="text-sm text-muted-foreground mt-1">{{ cpuBrand }}</div>
|
||||
<div class="flex items-center justify-between mt-2">
|
||||
<div class="text-sm">
|
||||
<span class="text-foreground">Overall Load: </span>
|
||||
<span class="font-semibold" style="color: var(--color-orange, #ff8c2f)">{{ overallLoad }}%</span>
|
||||
</div>
|
||||
<Button
|
||||
@click="toggleDetails"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
{{ showDetails ? 'Hide details' : 'Show details' }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CPU Cores Details -->
|
||||
<Transition name="slide-fade">
|
||||
<div v-if="showDetails" class="bg-muted/30 rounded-md p-4">
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3">
|
||||
<div
|
||||
v-for="core in cpuCores"
|
||||
:key="core.index"
|
||||
class="bg-background rounded border border-border p-2"
|
||||
>
|
||||
<div class="text-xs text-muted-foreground">
|
||||
CPU {{ core.index }} - HT {{ core.htIndex }}
|
||||
</div>
|
||||
<div class="text-sm font-semibold mt-1" style="color: var(--color-orange, #ff8c2f)">{{ core.percent }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- Chart Section -->
|
||||
<div class="border-t border-border pt-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h4 class="text-sm font-semibold text-foreground">CPU Usage</h4>
|
||||
<Select
|
||||
v-model="historyDuration"
|
||||
:items="historyOptions"
|
||||
placeholder="Duration"
|
||||
class="w-32"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="chartData.labels && chartData.labels.length > 0" class="h-40">
|
||||
<Line :data="chartData" :options="chartOptions" />
|
||||
</div>
|
||||
<div v-else class="h-40 flex items-center justify-center text-muted-foreground text-sm">
|
||||
Collecting data...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Smooth transition for details panel */
|
||||
.slide-fade-enter-active,
|
||||
.slide-fade-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.slide-fade-enter-from {
|
||||
transform: translateY(-10px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-fade-leave-to {
|
||||
transform: translateY(-10px);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
30
web/src/components/CpuStats/cpu-stats.query.ts
Normal file
30
web/src/components/CpuStats/cpu-stats.query.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { graphql } from '~/composables/gql/gql';
|
||||
|
||||
export const GET_CPU_INFO = graphql(/* GraphQL */ `
|
||||
query GetCpuInfo {
|
||||
info {
|
||||
cpu {
|
||||
id
|
||||
manufacturer
|
||||
brand
|
||||
vendor
|
||||
family
|
||||
model
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
export const CPU_METRICS_SUBSCRIPTION = graphql(/* GraphQL */ `
|
||||
subscription CpuMetrics {
|
||||
systemMetricsCpu {
|
||||
id
|
||||
percentTotal
|
||||
cpus {
|
||||
percentTotal
|
||||
percentUser
|
||||
percentSystem
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
} from '@unraid/ui';
|
||||
import { Settings } from 'lucide-vue-next';
|
||||
|
||||
import ConfirmDialog from '~/components/ConfirmDialog.vue';
|
||||
import {
|
||||
archiveAllNotifications,
|
||||
deleteArchivedNotifications,
|
||||
@@ -37,10 +38,12 @@ import NotificationsList from '~/components/Notifications/List.vue';
|
||||
import { useTrackLatestSeenNotification } from '~/composables/api/use-notifications';
|
||||
import { useFragment } from '~/composables/gql';
|
||||
import { NotificationImportance as Importance, NotificationType } from '~/composables/gql/graphql';
|
||||
import { useConfirm } from '~/composables/useConfirm';
|
||||
|
||||
const { mutate: archiveAll, loading: loadingArchiveAll } = useMutation(archiveAllNotifications);
|
||||
const { mutate: deleteArchives, loading: loadingDeleteAll } = useMutation(deleteArchivedNotifications);
|
||||
const { mutate: recalculateOverview } = useMutation(resetOverview);
|
||||
const { confirm } = useConfirm();
|
||||
const importance = ref<Importance | undefined>(undefined);
|
||||
|
||||
const filterItems = [
|
||||
@@ -52,17 +55,26 @@ const filterItems = [
|
||||
];
|
||||
|
||||
const confirmAndArchiveAll = async () => {
|
||||
if (confirm('This will archive all notifications on your Unraid server. Continue?')) {
|
||||
const confirmed = await confirm({
|
||||
title: 'Archive All Notifications',
|
||||
description: 'This will archive all notifications on your Unraid server. Continue?',
|
||||
confirmText: 'Archive All',
|
||||
confirmVariant: 'primary',
|
||||
});
|
||||
if (confirmed) {
|
||||
await archiveAll();
|
||||
}
|
||||
};
|
||||
|
||||
const confirmAndDeleteArchives = async () => {
|
||||
if (
|
||||
confirm(
|
||||
'This will permanently delete all archived notifications currently on your Unraid server. Continue?'
|
||||
)
|
||||
) {
|
||||
const confirmed = await confirm({
|
||||
title: 'Delete All Archived Notifications',
|
||||
description:
|
||||
'This will permanently delete all archived notifications currently on your Unraid server. This action cannot be undone.',
|
||||
confirmText: 'Delete All',
|
||||
confirmVariant: 'destructive',
|
||||
});
|
||||
if (confirmed) {
|
||||
await deleteArchives();
|
||||
}
|
||||
};
|
||||
@@ -230,4 +242,7 @@ const prepareToViewNotifications = () => {
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
<!-- Global Confirm Dialog -->
|
||||
<ConfirmDialog />
|
||||
</template>
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import SsoButtons from '~/components/sso/SsoButtons.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SsoButtons />
|
||||
</template>
|
||||
|
||||
<!-- Font size overrides are handled in component-registry.ts for custom elements -->
|
||||
55
web/src/components/SsoButton.standalone.vue
Normal file
55
web/src/components/SsoButton.standalone.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
|
||||
import SsoButtons from '~/components/sso/SsoButtons.vue';
|
||||
|
||||
const handleSsoStatus = (status: { checking: boolean; loading: boolean }) => {
|
||||
// Find the password recovery link on the page
|
||||
const passwordRecoveryLink = document.querySelector(
|
||||
'a[href*="lost-root-password"]'
|
||||
) as HTMLAnchorElement;
|
||||
|
||||
if (passwordRecoveryLink) {
|
||||
// Hide the link when checking API or loading
|
||||
if (status.checking || status.loading) {
|
||||
passwordRecoveryLink.style.display = 'none';
|
||||
} else {
|
||||
// Show it again when not checking/loading
|
||||
passwordRecoveryLink.style.display = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Also hide password recovery initially while checking
|
||||
onMounted(() => {
|
||||
const passwordRecoveryLink = document.querySelector(
|
||||
'a[href*="lost-root-password"]'
|
||||
) as HTMLAnchorElement;
|
||||
if (passwordRecoveryLink) {
|
||||
// Store original display value
|
||||
const originalDisplay = passwordRecoveryLink.style.display || '';
|
||||
passwordRecoveryLink.dataset.originalDisplay = originalDisplay;
|
||||
}
|
||||
});
|
||||
|
||||
// Restore on unmount
|
||||
onUnmounted(() => {
|
||||
const passwordRecoveryLink = document.querySelector(
|
||||
'a[href*="lost-root-password"]'
|
||||
) as HTMLAnchorElement;
|
||||
if (passwordRecoveryLink && passwordRecoveryLink.dataset.originalDisplay !== undefined) {
|
||||
passwordRecoveryLink.style.display = passwordRecoveryLink.dataset.originalDisplay;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SsoButtons @sso-status="handleSsoStatus" />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* Global input text color when SSO button is present (for login page) */
|
||||
body:has(unraid-sso-button) input {
|
||||
color: #1b1b1b !important;
|
||||
}
|
||||
</style>
|
||||
40
web/src/components/TestThemeSwitcher.standalone.vue
Normal file
40
web/src/components/TestThemeSwitcher.standalone.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
import { Select } from '@unraid/ui';
|
||||
|
||||
import type { Theme } from '~/themes/types';
|
||||
|
||||
import { useThemeStore } from '~/store/theme';
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
const { theme } = storeToRefs(themeStore);
|
||||
|
||||
// Available theme options
|
||||
const items = [
|
||||
{ value: 'white', label: 'Light' },
|
||||
{ value: 'black', label: 'Dark' },
|
||||
{ value: 'azure', label: 'Azure' },
|
||||
{ value: 'gray', label: 'Gray' },
|
||||
];
|
||||
|
||||
// Current theme value
|
||||
const currentTheme = computed({
|
||||
get: () => theme.value.name,
|
||||
set: (value: string) => {
|
||||
const newTheme: Theme = {
|
||||
...theme.value,
|
||||
name: value,
|
||||
};
|
||||
themeStore.setTheme(newTheme);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-medium text-white">Theme:</span>
|
||||
<Select v-model="currentTheme" :items="items" placeholder="Select theme" class="w-32" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,6 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { Toaster } from '@unraid/ui';
|
||||
|
||||
import { useThemeStore } from '~/store/theme';
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
// Get dark mode from theme store
|
||||
const theme = computed(() => (themeStore.darkMode ? 'dark' : 'light'));
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
position?:
|
||||
@@ -18,5 +27,5 @@ withDefaults(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Toaster rich-colors close-button :position="position" />
|
||||
<Toaster rich-colors close-button :position="position" :theme="theme" />
|
||||
</template>
|
||||
|
||||
@@ -2,27 +2,30 @@
|
||||
import { computed, onBeforeMount, ref, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
import { ArrowDownTrayIcon } from '@heroicons/vue/24/outline';
|
||||
import {
|
||||
ArrowTopRightOnSquareIcon,
|
||||
CogIcon,
|
||||
EyeIcon,
|
||||
IdentificationIcon,
|
||||
KeyIcon,
|
||||
XMarkIcon,
|
||||
} from '@heroicons/vue/24/solid';
|
||||
import {
|
||||
BrandButton,
|
||||
BrandLoading,
|
||||
Button,
|
||||
cn,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogRoot,
|
||||
DialogTitle,
|
||||
Label,
|
||||
ResponsiveModal,
|
||||
ResponsiveModalFooter,
|
||||
ResponsiveModalHeader,
|
||||
ResponsiveModalTitle,
|
||||
Switch,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@unraid/ui';
|
||||
|
||||
import type { BrandButtonProps } from '@unraid/ui';
|
||||
@@ -96,6 +99,9 @@ watch(updateOsIgnoredReleases, (newVal, oldVal) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Get the localized 'Close' text for comparison
|
||||
const localizedCloseText = computed(() => props.t('Close'));
|
||||
|
||||
const notificationsSettings = computed(() => {
|
||||
return !updateOsNotificationsEnabled.value
|
||||
? props.t(
|
||||
@@ -115,30 +121,18 @@ const modalCopy = computed((): ModalCopy | null => {
|
||||
};
|
||||
}
|
||||
|
||||
// Use the release date
|
||||
let formattedReleaseDate = '';
|
||||
if (availableReleaseDate.value) {
|
||||
// build string with prefix
|
||||
formattedReleaseDate = props.t('Release date {0}', [userFormattedReleaseDate.value]);
|
||||
}
|
||||
|
||||
if (availableWithRenewal.value) {
|
||||
const description = regUpdatesExpired.value
|
||||
? `${props.t('Eligible for updates released on or before {0}.', [formattedRegExp.value])} ${props.t('Extend your license to access the latest updates.')}`
|
||||
: props.t('Eligible for free feature updates until {0}', [formattedRegExp.value]);
|
||||
return {
|
||||
title: props.t('Unraid OS {0} Released', [availableWithRenewal.value]),
|
||||
description: `<p>${formattedReleaseDate}</p><p>${description}</p>`,
|
||||
title: props.t('Update Available'),
|
||||
description: description,
|
||||
};
|
||||
} else if (available.value) {
|
||||
const description = availableRequiresAuth.value
|
||||
? props.t('Release requires verification to update')
|
||||
: undefined;
|
||||
return {
|
||||
title: props.t('Unraid OS {0} Update Available', [available.value]),
|
||||
description: description
|
||||
? `<p>${formattedReleaseDate}</p><p>${description}</p>`
|
||||
: formattedReleaseDate,
|
||||
title: props.t('Update Available'),
|
||||
description: undefined,
|
||||
};
|
||||
} else if (!available.value && !availableWithRenewal.value) {
|
||||
return {
|
||||
@@ -169,8 +163,18 @@ const extraLinks = computed((): BrandButtonProps[] => {
|
||||
});
|
||||
|
||||
const actionButtons = computed((): BrandButtonProps[] | null => {
|
||||
// If ignoring release, show close button as primary action
|
||||
if (ignoreThisRelease.value && (available.value || availableWithRenewal.value)) {
|
||||
return [
|
||||
{
|
||||
click: () => close(),
|
||||
text: props.t('Close'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// update not available or no action buttons default closing
|
||||
if (!available.value || ignoreThisRelease.value) {
|
||||
if (!available.value && !availableWithRenewal.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -274,20 +278,58 @@ const modalWidth = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogRoot :open="open" @update:open="(value) => !value && close()">
|
||||
<DialogContent :class="modalWidth" :show-close-button="!checkForUpdatesLoading">
|
||||
<DialogHeader v-if="modalCopy?.title">
|
||||
<DialogTitle>
|
||||
<ResponsiveModal
|
||||
:open="open"
|
||||
:dialog-class="modalWidth"
|
||||
:sheet-class="'h-full'"
|
||||
:show-close-button="!checkForUpdatesLoading"
|
||||
@update:open="(value) => !value && close()"
|
||||
>
|
||||
<div class="flex h-full flex-col">
|
||||
<ResponsiveModalHeader v-if="modalCopy?.title">
|
||||
<ResponsiveModalTitle>
|
||||
{{ modalCopy.title }}
|
||||
</DialogTitle>
|
||||
</ResponsiveModalTitle>
|
||||
<DialogDescription v-if="modalCopy?.description">
|
||||
<span v-html="modalCopy.description" />
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
</ResponsiveModalHeader>
|
||||
|
||||
<div v-if="renderMainSlot" class="flex flex-col gap-4">
|
||||
<div v-if="renderMainSlot" class="flex flex-1 flex-col gap-6 overflow-y-auto px-6">
|
||||
<BrandLoading v-if="checkForUpdatesLoading" class="mx-auto w-[150px]" />
|
||||
<div v-else class="flex flex-col gap-y-4">
|
||||
<div v-else class="flex flex-col gap-y-6">
|
||||
<!-- OS Update highlight section -->
|
||||
<div v-if="available || availableWithRenewal" class="flex flex-col items-center gap-4 py-4">
|
||||
<div class="bg-primary/10 flex items-center justify-center rounded-full p-4">
|
||||
<ArrowDownTrayIcon class="text-primary h-8 w-8" />
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<h2 class="text-foreground text-3xl font-bold">
|
||||
{{ availableWithRenewal || available }}
|
||||
</h2>
|
||||
<p v-if="userFormattedReleaseDate" class="text-muted-foreground mt-2 text-center text-sm">
|
||||
Released on {{ userFormattedReleaseDate }}
|
||||
</p>
|
||||
<p
|
||||
v-if="availableRequiresAuth && !availableWithRenewal"
|
||||
class="mt-2 text-center text-sm text-amber-500"
|
||||
>
|
||||
{{ t('Requires verification to update') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<div
|
||||
class="hover:bg-muted/50 flex cursor-pointer items-center gap-3 rounded-lg p-2 transition-colors"
|
||||
@click="ignoreThisRelease = !ignoreThisRelease"
|
||||
>
|
||||
<Switch v-model="ignoreThisRelease" @click.stop />
|
||||
<Label class="text-muted-foreground cursor-pointer text-sm">
|
||||
{{ t('Ignore this release until next reboot') }}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="extraLinks.length > 0"
|
||||
:class="cn('xs:!flex-row flex flex-col justify-center gap-2')"
|
||||
@@ -295,7 +337,7 @@ const modalWidth = computed(() => {
|
||||
<BrandButton
|
||||
v-for="item in extraLinks"
|
||||
:key="item.text"
|
||||
:btn-style="item.variant ?? undefined"
|
||||
:variant="item.variant ?? undefined"
|
||||
:href="item.href ?? undefined"
|
||||
:icon="item.icon"
|
||||
:icon-right="item.iconRight"
|
||||
@@ -306,23 +348,8 @@ const modalWidth = computed(() => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="available || availableWithRenewal" class="mx-auto">
|
||||
<div class="flex items-center justify-center gap-2 rounded p-2">
|
||||
<Switch
|
||||
v-model="ignoreThisRelease"
|
||||
:class="
|
||||
ignoreThisRelease
|
||||
? 'from-unraid-red to-orange bg-linear-to-r'
|
||||
: 'data-[state=unchecked]:bg-opacity-10 data-[state=unchecked]:bg-foreground data-[state=unchecked]:bg-transparent'
|
||||
"
|
||||
/>
|
||||
<Label class="text-base">
|
||||
{{ t('Ignore this release until next reboot') }}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="updateOsIgnoredReleases.length > 0"
|
||||
v-if="updateOsIgnoredReleases.length > 0 && !(available || availableWithRenewal)"
|
||||
class="mx-auto flex w-full max-w-[640px] flex-col gap-2"
|
||||
>
|
||||
<h3 class="text-left text-base font-semibold italic">
|
||||
@@ -338,7 +365,7 @@ const modalWidth = computed(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<ResponsiveModalFooter>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
@@ -347,31 +374,75 @@ const modalWidth = computed(() => {
|
||||
)
|
||||
"
|
||||
>
|
||||
<div :class="cn('xs:!flex-row flex flex-col-reverse justify-start gap-2')">
|
||||
<Button variant="ghost" @click="close">
|
||||
<XMarkIcon class="mr-2 h-4 w-4" />
|
||||
{{ t('Close') }}
|
||||
</Button>
|
||||
<Button variant="ghost" @click="accountStore.updateOs()">
|
||||
<ArrowTopRightOnSquareIcon class="mr-2 h-4 w-4" />
|
||||
{{ t('More options') }}
|
||||
</Button>
|
||||
<div
|
||||
v-if="actionButtons"
|
||||
:class="cn('xs:!flex-row flex flex-col-reverse justify-start gap-3')"
|
||||
>
|
||||
<TooltipProvider>
|
||||
<Tooltip :delay-duration="0">
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" @click="accountStore.updateOs()">
|
||||
<ArrowTopRightOnSquareIcon class="mr-2 h-4 w-4" />
|
||||
{{ t('More Options') }}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent class="max-w-xs">
|
||||
<div class="flex items-start gap-2">
|
||||
<ArrowTopRightOnSquareIcon
|
||||
class="text-muted-foreground mt-0.5 h-4 w-4 flex-shrink-0"
|
||||
/>
|
||||
<p class="text-left text-sm">
|
||||
{{
|
||||
t(
|
||||
'Manage update preferences including beta access and version selection at account.unraid.net'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<div v-if="actionButtons" :class="cn('xs:!flex-row flex flex-col justify-end gap-2')">
|
||||
<BrandButton
|
||||
v-for="item in actionButtons"
|
||||
:key="item.text"
|
||||
:btn-style="item.variant ?? undefined"
|
||||
:icon="item.icon"
|
||||
:icon-right="item.iconRight"
|
||||
:icon-right-hover-display="item.iconRightHoverDisplay"
|
||||
:text="t(item.text ?? '')"
|
||||
:title="item.title ? t(item.title) : undefined"
|
||||
@click="item.click?.()"
|
||||
/>
|
||||
<div v-if="actionButtons" :class="cn('xs:!flex-row flex flex-col justify-end gap-3')">
|
||||
<template v-for="item in actionButtons" :key="item.text">
|
||||
<TooltipProvider v-if="ignoreThisRelease && item.text === localizedCloseText">
|
||||
<Tooltip :delay-duration="300">
|
||||
<TooltipTrigger as-child>
|
||||
<BrandButton
|
||||
:variant="item.variant ?? undefined"
|
||||
:icon="item.icon"
|
||||
:icon-right="item.iconRight"
|
||||
:icon-right-hover-display="item.iconRightHoverDisplay"
|
||||
:text="item.text ?? ''"
|
||||
:title="item.title ? item.title : undefined"
|
||||
@click="item.click?.()"
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
{{
|
||||
t(
|
||||
'You can opt back in to an ignored release by clicking on the Check for Updates button in the header anytime'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<BrandButton
|
||||
v-else
|
||||
:variant="item.variant ?? undefined"
|
||||
:icon="item.icon"
|
||||
:icon-right="item.iconRight"
|
||||
:icon-right-hover-display="item.iconRightHoverDisplay"
|
||||
:text="item.text ?? ''"
|
||||
:title="item.title ? item.title : undefined"
|
||||
@click="item.click?.()"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</DialogRoot>
|
||||
</ResponsiveModalFooter>
|
||||
</div>
|
||||
</ResponsiveModal>
|
||||
</template>
|
||||
|
||||
292
web/src/components/UpdateOs/TestUpdateModal.standalone.vue
Normal file
292
web/src/components/UpdateOs/TestUpdateModal.standalone.vue
Normal file
@@ -0,0 +1,292 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { Button, Label, Switch } from '@unraid/ui';
|
||||
|
||||
import type { ServerState, ServerUpdateOsResponse } from '~/types/server';
|
||||
|
||||
import CheckUpdateResponseModal from '~/components/UpdateOs/CheckUpdateResponseModal.vue';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import { useUpdateOsStore } from '~/store/updateOs';
|
||||
|
||||
const { t } = useI18n();
|
||||
const updateOsStore = useUpdateOsStore();
|
||||
const serverStore = useServerStore();
|
||||
|
||||
// Test scenarios
|
||||
const testScenarios = [
|
||||
{
|
||||
id: 'expired-ineligible',
|
||||
name: 'Expired key with ineligible update',
|
||||
description: 'License expired, update available but not eligible',
|
||||
serverState: 'EEXPIRED' as ServerState,
|
||||
updateResponse: {
|
||||
version: '7.1.0',
|
||||
name: 'Unraid 7.1.0',
|
||||
date: '2024-12-15',
|
||||
isNewer: true,
|
||||
isEligible: false,
|
||||
changelog:
|
||||
'https://raw.githubusercontent.com/unraid/docs/main/docs/unraid-os/release-notes/7.1.0.md',
|
||||
changelogPretty: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
|
||||
sha256: undefined, // requires auth
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'normal-update',
|
||||
name: 'Normal update available',
|
||||
description: 'Active license with eligible update',
|
||||
serverState: 'BASIC' as ServerState,
|
||||
updateResponse: {
|
||||
version: '7.1.0',
|
||||
name: 'Unraid 7.1.0',
|
||||
date: '2024-12-15',
|
||||
isNewer: true,
|
||||
isEligible: true,
|
||||
changelog:
|
||||
'https://raw.githubusercontent.com/unraid/docs/main/docs/unraid-os/release-notes/7.1.0.md',
|
||||
changelogPretty: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
|
||||
sha256: 'abc123def456789',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'renewal-required',
|
||||
name: 'Update requires renewal',
|
||||
description: 'License expired > 1 year, update requires renewal',
|
||||
serverState: 'STARTER' as ServerState,
|
||||
updateResponse: {
|
||||
version: '7.1.0',
|
||||
name: 'Unraid 7.1.0',
|
||||
date: '2024-12-15',
|
||||
isNewer: true,
|
||||
isEligible: false,
|
||||
changelog:
|
||||
'https://raw.githubusercontent.com/unraid/docs/main/docs/unraid-os/release-notes/7.1.0.md',
|
||||
changelogPretty: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
|
||||
sha256: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'no-update',
|
||||
name: 'No update available',
|
||||
description: 'Already on latest version',
|
||||
serverState: 'BASIC' as ServerState,
|
||||
updateResponse: {
|
||||
version: '7.0.0',
|
||||
name: 'Unraid 7.0.0',
|
||||
date: '2024-01-15',
|
||||
isNewer: false,
|
||||
isEligible: true,
|
||||
changelog:
|
||||
'https://raw.githubusercontent.com/unraid/docs/main/docs/unraid-os/release-notes/7.0.0.md',
|
||||
sha256: 'xyz789abc123',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'trial-update',
|
||||
name: 'Trial with update',
|
||||
description: 'Trial license with update available',
|
||||
serverState: 'TRIAL' as ServerState,
|
||||
updateResponse: {
|
||||
version: '7.1.0',
|
||||
name: 'Unraid 7.1.0',
|
||||
date: '2024-12-15',
|
||||
isNewer: true,
|
||||
isEligible: true,
|
||||
changelog: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
|
||||
sha256: 'def456ghi789',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'pro-auth-required',
|
||||
name: 'Pro license - auth required',
|
||||
description: 'Pro license but authentication required for download',
|
||||
serverState: 'PRO' as ServerState,
|
||||
updateResponse: {
|
||||
version: '7.1.0',
|
||||
name: 'Unraid 7.1.0',
|
||||
date: '2024-12-15',
|
||||
isNewer: true,
|
||||
isEligible: true,
|
||||
changelog: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
|
||||
sha256: undefined, // requires auth
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Component state
|
||||
const selectedScenario = ref('normal-update');
|
||||
const ignoreRelease = ref(false);
|
||||
const checkingForUpdates = ref(false);
|
||||
const ignoredReleases = ref<string[]>([]);
|
||||
|
||||
// Use the store's modal state directly
|
||||
const modalOpen = computed({
|
||||
get: () => updateOsStore.updateOsModalVisible,
|
||||
set: (value) => updateOsStore.setModalOpen(value),
|
||||
});
|
||||
|
||||
// Apply scenario
|
||||
const applyScenario = () => {
|
||||
const scenario = testScenarios.find((s) => s.id === selectedScenario.value);
|
||||
if (!scenario) return;
|
||||
|
||||
// Set server state
|
||||
const currentTime = Date.now();
|
||||
const expiredTime = scenario.serverState === 'EEXPIRED' ? currentTime - 24 * 60 * 60 * 1000 : 0;
|
||||
const regExp =
|
||||
scenario.serverState === 'STARTER' ? currentTime - 400 * 24 * 60 * 60 * 1000 : undefined;
|
||||
|
||||
// Apply update response
|
||||
if (scenario.serverState === 'EEXPIRED') {
|
||||
serverStore.$patch({
|
||||
expireTime: expiredTime,
|
||||
state: scenario.serverState,
|
||||
regExp: undefined,
|
||||
});
|
||||
} else if (scenario.serverState === 'STARTER') {
|
||||
serverStore.$patch({
|
||||
state: scenario.serverState,
|
||||
regExp: regExp,
|
||||
regTy: 'Starter',
|
||||
});
|
||||
} else {
|
||||
serverStore.$patch({
|
||||
state: scenario.serverState,
|
||||
regExp: undefined,
|
||||
expireTime: scenario.serverState === 'TRIAL' ? currentTime + 7 * 24 * 60 * 60 * 1000 : 0,
|
||||
});
|
||||
}
|
||||
|
||||
serverStore.setUpdateOsResponse(scenario.updateResponse as ServerUpdateOsResponse);
|
||||
|
||||
// Apply ignored releases
|
||||
if (ignoreRelease.value && scenario.updateResponse.isNewer) {
|
||||
if (!ignoredReleases.value.includes(scenario.updateResponse.version)) {
|
||||
ignoredReleases.value.push(scenario.updateResponse.version);
|
||||
}
|
||||
} else {
|
||||
ignoredReleases.value = ignoredReleases.value.filter((v) => v !== scenario.updateResponse.version);
|
||||
}
|
||||
serverStore.$patch({ updateOsIgnoredReleases: ignoredReleases.value });
|
||||
};
|
||||
|
||||
// Watch for scenario changes
|
||||
watch([selectedScenario, ignoreRelease], () => {
|
||||
applyScenario();
|
||||
});
|
||||
|
||||
// Watch for loading state changes
|
||||
watch(checkingForUpdates, (value) => {
|
||||
updateOsStore.checkForUpdatesLoading = value;
|
||||
});
|
||||
|
||||
// Open modal with scenario
|
||||
const openModal = () => {
|
||||
applyScenario();
|
||||
updateOsStore.checkForUpdatesLoading = checkingForUpdates.value;
|
||||
updateOsStore.setModalOpen(true);
|
||||
};
|
||||
|
||||
// Initialize
|
||||
applyScenario();
|
||||
|
||||
const currentScenario = computed(() => testScenarios.find((s) => s.id === selectedScenario.value));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container mx-auto max-w-4xl p-6">
|
||||
<div class="rounded-lg bg-white p-6 shadow-lg dark:bg-zinc-900">
|
||||
<div class="mb-6">
|
||||
<h2 class="mb-2 text-2xl font-bold">Update Modal Test Page</h2>
|
||||
<p class="text-muted-foreground">
|
||||
Test various update scenarios for the CheckUpdateResponseModal component
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Scenario Selection -->
|
||||
<div class="space-y-4">
|
||||
<Label class="text-lg font-semibold">Select Test Scenario</Label>
|
||||
<div class="space-y-3">
|
||||
<div v-for="scenario in testScenarios" :key="scenario.id" class="flex items-start space-x-3">
|
||||
<input
|
||||
type="radio"
|
||||
:id="scenario.id"
|
||||
:value="scenario.id"
|
||||
v-model="selectedScenario"
|
||||
class="mt-1 rounded-full"
|
||||
/>
|
||||
<div class="flex-1">
|
||||
<Label :for="scenario.id" class="block cursor-pointer font-medium">
|
||||
{{ scenario.name }}
|
||||
</Label>
|
||||
<p class="text-muted-foreground mt-1 text-sm">{{ scenario.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Options -->
|
||||
<div class="space-y-4 border-t pt-4">
|
||||
<h3 class="font-semibold">Options</h3>
|
||||
|
||||
<div class="flex items-center space-x-3">
|
||||
<Switch id="ignore-release" v-model:checked="ignoreRelease" />
|
||||
<Label for="ignore-release" class="cursor-pointer">Ignore this release</Label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-3">
|
||||
<Switch id="checking-updates" v-model:checked="checkingForUpdates" />
|
||||
<Label for="checking-updates" class="cursor-pointer"
|
||||
>Show checking for updates loading state</Label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current State Display -->
|
||||
<div class="space-y-2 border-t pt-4">
|
||||
<h3 class="font-semibold">Current Scenario Details</h3>
|
||||
<div class="space-y-1 font-mono text-sm">
|
||||
<p><span class="font-semibold">Server State:</span> {{ currentScenario?.serverState }}</p>
|
||||
<p>
|
||||
<span class="font-semibold">Version:</span> {{ currentScenario?.updateResponse.version }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="font-semibold">Is Newer:</span> {{ currentScenario?.updateResponse.isNewer }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="font-semibold">Is Eligible:</span>
|
||||
{{ currentScenario?.updateResponse.isEligible }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="font-semibold">Has SHA256:</span>
|
||||
{{ !!currentScenario?.updateResponse.sha256 }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="font-semibold">Ignored Releases:</span>
|
||||
{{ ignoredReleases.join(', ') || 'None' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Open Modal Button -->
|
||||
<div class="border-t pt-4">
|
||||
<Button @click="openModal" variant="primary" class="w-full"> Open Update Modal </Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- The Modal Component -->
|
||||
<CheckUpdateResponseModal
|
||||
:open="modalOpen"
|
||||
@update:open="
|
||||
(val: boolean) => {
|
||||
modalOpen = val;
|
||||
}
|
||||
"
|
||||
:t="t"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -3,7 +3,21 @@
|
||||
|
||||
import { provideApolloClient } from '@vue/apollo-composable';
|
||||
|
||||
import { ensureTeleportContainer } from '@unraid/ui';
|
||||
// Copy the ensureTeleportContainer function to avoid importing from @unraid/ui
|
||||
// which causes ESM/CommonJS issues with ajv-errors
|
||||
function ensureTeleportContainer(): HTMLElement {
|
||||
const containerId = 'unraid-teleport-container';
|
||||
let container = document.getElementById(containerId);
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.id = containerId;
|
||||
container.style.position = 'relative';
|
||||
container.classList.add('unapi');
|
||||
container.style.zIndex = '999999';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
return container;
|
||||
}
|
||||
import {
|
||||
autoMountAllComponents,
|
||||
autoMountComponent,
|
||||
|
||||
@@ -6,13 +6,16 @@ import type { Component } from 'vue';
|
||||
|
||||
// Import CSS for bundling - this ensures Tailwind styles are included
|
||||
import '~/assets/main.css';
|
||||
// Import @unraid/ui styles which includes vue-sonner styles
|
||||
import '@unraid/ui/styles';
|
||||
|
||||
// Static imports for critical components that are always present
|
||||
// These are included in the main bundle for faster initial render
|
||||
import HeaderOsVersionCe from '@/components/HeaderOsVersion.ce.vue';
|
||||
import ModalsCe from '@/components/Modals.ce.vue';
|
||||
import ThemeSwitcherCe from '@/components/ThemeSwitcher.ce.vue';
|
||||
import UserProfileCe from '@/components/UserProfile.ce.vue';
|
||||
import HeaderOsVersionCe from '@/components/HeaderOsVersion.standalone.vue';
|
||||
import ModalsCe from '@/components/Modals.standalone.vue';
|
||||
import ThemeSwitcherCe from '@/components/ThemeSwitcher.standalone.vue';
|
||||
import UnraidToaster from '@/components/UnraidToaster.vue';
|
||||
import UserProfileCe from '@/components/UserProfile.standalone.vue';
|
||||
|
||||
// Type for Vue component module
|
||||
type VueComponentModule = { default: object } | object;
|
||||
@@ -31,17 +34,17 @@ export type ComponentMapping = {
|
||||
// Page-specific components use dynamic imports (lazy loaded)
|
||||
export const componentMappings: ComponentMapping[] = [
|
||||
{
|
||||
loader: () => import('../Auth.ce.vue'),
|
||||
loader: () => import('../Auth.standalone.vue'),
|
||||
selector: 'unraid-auth',
|
||||
appId: 'auth',
|
||||
},
|
||||
{
|
||||
loader: () => import('../ConnectSettings/ConnectSettings.ce.vue'),
|
||||
loader: () => import('../ConnectSettings/ConnectSettings.standalone.vue'),
|
||||
selector: 'unraid-connect-settings',
|
||||
appId: 'connect-settings',
|
||||
},
|
||||
{
|
||||
loader: () => import('../DownloadApiLogs.ce.vue'),
|
||||
loader: () => import('../DownloadApiLogs.standalone.vue'),
|
||||
selector: 'unraid-download-api-logs',
|
||||
appId: 'download-api-logs',
|
||||
},
|
||||
@@ -61,42 +64,42 @@ export const componentMappings: ComponentMapping[] = [
|
||||
appId: 'user-profile',
|
||||
},
|
||||
{
|
||||
loader: () => import('../Registration.ce.vue'),
|
||||
loader: () => import('../Registration.standalone.vue'),
|
||||
selector: 'unraid-registration',
|
||||
appId: 'registration',
|
||||
},
|
||||
{
|
||||
loader: () => import('../WanIpCheck.ce.vue'),
|
||||
loader: () => import('../WanIpCheck.standalone.vue'),
|
||||
selector: 'unraid-wan-ip-check',
|
||||
appId: 'wan-ip-check',
|
||||
},
|
||||
{
|
||||
loader: () => import('../CallbackHandler.ce.vue'),
|
||||
loader: () => import('../CallbackHandler.standalone.vue'),
|
||||
selector: 'unraid-callback-handler',
|
||||
appId: 'callback-handler',
|
||||
},
|
||||
{
|
||||
loader: () => import('../Logs/LogViewer.ce.vue'),
|
||||
loader: () => import('../Logs/LogViewer.standalone.vue'),
|
||||
selector: 'unraid-log-viewer',
|
||||
appId: 'log-viewer',
|
||||
},
|
||||
{
|
||||
loader: () => import('../SsoButton.ce.vue'),
|
||||
loader: () => import('../SsoButton.standalone.vue'),
|
||||
selector: 'unraid-sso-button',
|
||||
appId: 'sso-button',
|
||||
},
|
||||
{
|
||||
loader: () => import('../Activation/WelcomeModal.ce.vue'),
|
||||
loader: () => import('../Activation/WelcomeModal.standalone.vue'),
|
||||
selector: 'unraid-welcome-modal',
|
||||
appId: 'welcome-modal',
|
||||
},
|
||||
{
|
||||
loader: () => import('../UpdateOs.ce.vue'),
|
||||
loader: () => import('../UpdateOs.standalone.vue'),
|
||||
selector: 'unraid-update-os',
|
||||
appId: 'update-os',
|
||||
},
|
||||
{
|
||||
loader: () => import('../DowngradeOs.ce.vue'),
|
||||
loader: () => import('../DowngradeOs.standalone.vue'),
|
||||
selector: 'unraid-downgrade-os',
|
||||
appId: 'downgrade-os',
|
||||
},
|
||||
@@ -106,22 +109,22 @@ export const componentMappings: ComponentMapping[] = [
|
||||
appId: 'dev-settings',
|
||||
},
|
||||
{
|
||||
loader: () => import('../ApiKeyPage.ce.vue'),
|
||||
loader: () => import('../ApiKeyPage.standalone.vue'),
|
||||
selector: ['unraid-apikey-page', 'unraid-api-key-manager'],
|
||||
appId: 'apikey-page',
|
||||
},
|
||||
{
|
||||
loader: () => import('../ApiKeyAuthorize.ce.vue'),
|
||||
loader: () => import('../ApiKeyAuthorize.standalone.vue'),
|
||||
selector: 'unraid-apikey-authorize',
|
||||
appId: 'apikey-authorize',
|
||||
},
|
||||
{
|
||||
loader: () => import('../DevModalTest.ce.vue'),
|
||||
loader: () => import('../DevModalTest.standalone.vue'),
|
||||
selector: 'unraid-dev-modal-test',
|
||||
appId: 'dev-modal-test',
|
||||
},
|
||||
{
|
||||
loader: () => import('../LayoutViews/Detail/DetailTest.ce.vue'),
|
||||
loader: () => import('../LayoutViews/Detail/DetailTest.standalone.vue'),
|
||||
selector: 'unraid-detail-test',
|
||||
appId: 'detail-test',
|
||||
},
|
||||
@@ -131,13 +134,28 @@ export const componentMappings: ComponentMapping[] = [
|
||||
appId: 'theme-switcher',
|
||||
},
|
||||
{
|
||||
loader: () => import('../ColorSwitcher.ce.vue'),
|
||||
loader: () => import('../ColorSwitcher.standalone.vue'),
|
||||
selector: 'unraid-color-switcher',
|
||||
appId: 'color-switcher',
|
||||
},
|
||||
{
|
||||
loader: () => import('../UnraidToaster.vue'),
|
||||
component: UnraidToaster, // Static import - toaster styles need to be in main bundle
|
||||
selector: ['unraid-toaster', 'uui-toaster'],
|
||||
appId: 'toaster',
|
||||
},
|
||||
{
|
||||
loader: () => import('../UpdateOs/TestUpdateModal.standalone.vue'),
|
||||
selector: 'unraid-test-update-modal',
|
||||
appId: 'test-update-modal',
|
||||
},
|
||||
{
|
||||
loader: () => import('../TestThemeSwitcher.standalone.vue'),
|
||||
selector: 'unraid-test-theme-switcher',
|
||||
appId: 'test-theme-switcher',
|
||||
},
|
||||
{
|
||||
loader: () => import('../CpuStats/CpuStats.standalone.vue'),
|
||||
selector: 'unraid-cpu-stats',
|
||||
appId: 'cpu-stats',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { computed, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import SsoProviderButton from '~/components/sso/SsoProviderButton.vue';
|
||||
import { useSsoAuth } from '~/components/sso/useSsoAuth';
|
||||
import { useSsoProviders } from '~/components/sso/useSsoProviders';
|
||||
|
||||
const emit = defineEmits<{
|
||||
'sso-status': [status: { checking: boolean; loading: boolean }];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const { oidcProviders, hasProviders, checkingApi } = useSsoProviders();
|
||||
const { currentState, error, navigateToProvider } = useSsoAuth();
|
||||
@@ -13,6 +17,15 @@ const { currentState, error, navigateToProvider } = useSsoAuth();
|
||||
const showError = computed(() => currentState.value === 'error');
|
||||
const showOr = computed(() => (currentState.value === 'idle' || showError.value) && hasProviders.value);
|
||||
const isLoading = computed(() => currentState.value === 'loading');
|
||||
|
||||
// Emit status changes
|
||||
watch(
|
||||
[checkingApi, isLoading],
|
||||
([checking, loading]) => {
|
||||
emit('sso-status', { checking, loading });
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -28,6 +28,8 @@ type Documents = {
|
||||
"\n query GetPermissionsForRoles($roles: [Role!]!) {\n getPermissionsForRoles(roles: $roles) {\n resource\n actions\n }\n }\n": typeof types.GetPermissionsForRolesDocument,
|
||||
"\n query Unified {\n settings {\n unified {\n id\n dataSchema\n uiSchema\n values\n }\n }\n }\n": typeof types.UnifiedDocument,
|
||||
"\n mutation UpdateConnectSettings($input: JSON!) {\n updateSettings(input: $input) {\n restartRequired\n values\n }\n }\n": typeof types.UpdateConnectSettingsDocument,
|
||||
"\n query GetCpuInfo {\n info {\n cpu {\n id\n manufacturer\n brand\n vendor\n family\n model\n }\n }\n }\n": typeof types.GetCpuInfoDocument,
|
||||
"\n subscription CpuMetrics {\n systemMetricsCpu {\n id\n percentTotal\n cpus {\n percentTotal\n percentUser\n percentSystem\n }\n }\n }\n": typeof types.CpuMetricsDocument,
|
||||
"\n query LogFiles {\n logFiles {\n name\n path\n size\n modifiedAt\n }\n }\n": typeof types.LogFilesDocument,
|
||||
"\n query LogFileContent($path: String!, $lines: Int, $startLine: Int) {\n logFile(path: $path, lines: $lines, startLine: $startLine) {\n path\n content\n totalLines\n startLine\n }\n }\n": typeof types.LogFileContentDocument,
|
||||
"\n subscription LogFileSubscription($path: String!) {\n logFile(path: $path) {\n path\n content\n totalLines\n }\n }\n": typeof types.LogFileSubscriptionDocument,
|
||||
@@ -73,6 +75,8 @@ const documents: Documents = {
|
||||
"\n query GetPermissionsForRoles($roles: [Role!]!) {\n getPermissionsForRoles(roles: $roles) {\n resource\n actions\n }\n }\n": types.GetPermissionsForRolesDocument,
|
||||
"\n query Unified {\n settings {\n unified {\n id\n dataSchema\n uiSchema\n values\n }\n }\n }\n": types.UnifiedDocument,
|
||||
"\n mutation UpdateConnectSettings($input: JSON!) {\n updateSettings(input: $input) {\n restartRequired\n values\n }\n }\n": types.UpdateConnectSettingsDocument,
|
||||
"\n query GetCpuInfo {\n info {\n cpu {\n id\n manufacturer\n brand\n vendor\n family\n model\n }\n }\n }\n": types.GetCpuInfoDocument,
|
||||
"\n subscription CpuMetrics {\n systemMetricsCpu {\n id\n percentTotal\n cpus {\n percentTotal\n percentUser\n percentSystem\n }\n }\n }\n": types.CpuMetricsDocument,
|
||||
"\n query LogFiles {\n logFiles {\n name\n path\n size\n modifiedAt\n }\n }\n": types.LogFilesDocument,
|
||||
"\n query LogFileContent($path: String!, $lines: Int, $startLine: Int) {\n logFile(path: $path, lines: $lines, startLine: $startLine) {\n path\n content\n totalLines\n startLine\n }\n }\n": types.LogFileContentDocument,
|
||||
"\n subscription LogFileSubscription($path: String!) {\n logFile(path: $path) {\n path\n content\n totalLines\n }\n }\n": types.LogFileSubscriptionDocument,
|
||||
@@ -174,6 +178,14 @@ export function graphql(source: "\n query Unified {\n settings {\n unif
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation UpdateConnectSettings($input: JSON!) {\n updateSettings(input: $input) {\n restartRequired\n values\n }\n }\n"): (typeof documents)["\n mutation UpdateConnectSettings($input: JSON!) {\n updateSettings(input: $input) {\n restartRequired\n values\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query GetCpuInfo {\n info {\n cpu {\n id\n manufacturer\n brand\n vendor\n family\n model\n }\n }\n }\n"): (typeof documents)["\n query GetCpuInfo {\n info {\n cpu {\n id\n manufacturer\n brand\n vendor\n family\n model\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n subscription CpuMetrics {\n systemMetricsCpu {\n id\n percentTotal\n cpus {\n percentTotal\n percentUser\n percentSystem\n }\n }\n }\n"): (typeof documents)["\n subscription CpuMetrics {\n systemMetricsCpu {\n id\n percentTotal\n cpus {\n percentTotal\n percentUser\n percentSystem\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
||||
@@ -241,6 +241,8 @@ export type ArrayDisk = Node & {
|
||||
id: Scalars['PrefixedID']['output'];
|
||||
/** Array slot number. Parity1 is always 0 and Parity2 is always 29. Array slots will be 1 - 28. Cache slots are 30 - 53. Flash is 54. */
|
||||
idx: Scalars['Int']['output'];
|
||||
/** Whether the disk is currently spinning */
|
||||
isSpinning?: Maybe<Scalars['Boolean']['output']>;
|
||||
name?: Maybe<Scalars['String']['output']>;
|
||||
/** Number of unrecoverable errors reported by the device I/O drivers. Missing data due to unrecoverable array read errors is filled in on-the-fly using parity reconstruct (and we attempt to write this data back to the sector(s) which failed). Any unrecoverable write error results in disabling the disk. */
|
||||
numErrors?: Maybe<Scalars['BigInt']['output']>;
|
||||
@@ -607,6 +609,8 @@ export type Disk = Node & {
|
||||
id: Scalars['PrefixedID']['output'];
|
||||
/** The interface type of the disk */
|
||||
interfaceType: DiskInterfaceType;
|
||||
/** Whether the disk is spinning or not */
|
||||
isSpinning: Scalars['Boolean']['output'];
|
||||
/** The model name of the disk */
|
||||
name: Scalars['String']['output'];
|
||||
/** The partitions on the disk */
|
||||
@@ -674,6 +678,7 @@ export enum DiskSmartStatus {
|
||||
|
||||
export type Docker = Node & {
|
||||
__typename?: 'Docker';
|
||||
containerUpdateStatuses: Array<ExplicitStatusItem>;
|
||||
containers: Array<DockerContainer>;
|
||||
id: Scalars['PrefixedID']['output'];
|
||||
networks: Array<DockerNetwork>;
|
||||
@@ -699,6 +704,8 @@ export type DockerContainer = Node & {
|
||||
id: Scalars['PrefixedID']['output'];
|
||||
image: Scalars['String']['output'];
|
||||
imageId: Scalars['String']['output'];
|
||||
isRebuildReady?: Maybe<Scalars['Boolean']['output']>;
|
||||
isUpdateAvailable?: Maybe<Scalars['Boolean']['output']>;
|
||||
labels?: Maybe<Scalars['JSON']['output']>;
|
||||
mounts?: Maybe<Array<Scalars['JSON']['output']>>;
|
||||
names: Array<Scalars['String']['output']>;
|
||||
@@ -770,6 +777,12 @@ export type EnableDynamicRemoteAccessInput = {
|
||||
url: AccessUrlInput;
|
||||
};
|
||||
|
||||
export type ExplicitStatusItem = {
|
||||
__typename?: 'ExplicitStatusItem';
|
||||
name: Scalars['String']['output'];
|
||||
updateStatus: UpdateStatus;
|
||||
};
|
||||
|
||||
export type Flash = Node & {
|
||||
__typename?: 'Flash';
|
||||
guid: Scalars['String']['output'];
|
||||
@@ -1225,6 +1238,7 @@ export type Mutation = {
|
||||
rclone: RCloneMutations;
|
||||
/** Reads each notification to recompute & update the overview. */
|
||||
recalculateOverview: NotificationOverview;
|
||||
refreshDockerDigests: Scalars['Boolean']['output'];
|
||||
/** Remove one or more plugins from the API. Returns false if restart was triggered automatically, true if manual restart is required. */
|
||||
removePlugin: Scalars['Boolean']['output'];
|
||||
setDockerFolderChildren: ResolvedOrganizerV1;
|
||||
@@ -2260,6 +2274,14 @@ export type UpdateSettingsResponse = {
|
||||
warnings?: Maybe<Array<Scalars['String']['output']>>;
|
||||
};
|
||||
|
||||
/** Update status of a container. */
|
||||
export enum UpdateStatus {
|
||||
REBUILD_READY = 'REBUILD_READY',
|
||||
UNKNOWN = 'UNKNOWN',
|
||||
UPDATE_AVAILABLE = 'UPDATE_AVAILABLE',
|
||||
UP_TO_DATE = 'UP_TO_DATE'
|
||||
}
|
||||
|
||||
export type Uptime = {
|
||||
__typename?: 'Uptime';
|
||||
timestamp?: Maybe<Scalars['String']['output']>;
|
||||
@@ -2640,6 +2662,16 @@ export type UpdateConnectSettingsMutationVariables = Exact<{
|
||||
|
||||
export type UpdateConnectSettingsMutation = { __typename?: 'Mutation', updateSettings: { __typename?: 'UpdateSettingsResponse', restartRequired: boolean, values: any } };
|
||||
|
||||
export type GetCpuInfoQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type GetCpuInfoQuery = { __typename?: 'Query', info: { __typename?: 'Info', cpu: { __typename?: 'InfoCpu', id: string, manufacturer?: string | null, brand?: string | null, vendor?: string | null, family?: string | null, model?: string | null } } };
|
||||
|
||||
export type CpuMetricsSubscriptionVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type CpuMetricsSubscription = { __typename?: 'Subscription', systemMetricsCpu: { __typename?: 'CpuUtilization', id: string, percentTotal: number, cpus: Array<{ __typename?: 'CpuLoad', percentTotal: number, percentUser: number, percentSystem: number }> } };
|
||||
|
||||
export type LogFilesQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
@@ -2838,6 +2870,8 @@ export const PreviewEffectivePermissionsDocument = {"kind":"Document","definitio
|
||||
export const GetPermissionsForRolesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetPermissionsForRoles"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"roles"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Role"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getPermissionsForRoles"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"roles"},"value":{"kind":"Variable","name":{"kind":"Name","value":"roles"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"resource"}},{"kind":"Field","name":{"kind":"Name","value":"actions"}}]}}]}}]} as unknown as DocumentNode<GetPermissionsForRolesQuery, GetPermissionsForRolesQueryVariables>;
|
||||
export const UnifiedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Unified"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"settings"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unified"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSchema"}},{"kind":"Field","name":{"kind":"Name","value":"uiSchema"}},{"kind":"Field","name":{"kind":"Name","value":"values"}}]}}]}}]}}]} as unknown as DocumentNode<UnifiedQuery, UnifiedQueryVariables>;
|
||||
export const UpdateConnectSettingsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateConnectSettings"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"JSON"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateSettings"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"restartRequired"}},{"kind":"Field","name":{"kind":"Name","value":"values"}}]}}]}}]} as unknown as DocumentNode<UpdateConnectSettingsMutation, UpdateConnectSettingsMutationVariables>;
|
||||
export const GetCpuInfoDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetCpuInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"info"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cpu"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"manufacturer"}},{"kind":"Field","name":{"kind":"Name","value":"brand"}},{"kind":"Field","name":{"kind":"Name","value":"vendor"}},{"kind":"Field","name":{"kind":"Name","value":"family"}},{"kind":"Field","name":{"kind":"Name","value":"model"}}]}}]}}]}}]} as unknown as DocumentNode<GetCpuInfoQuery, GetCpuInfoQueryVariables>;
|
||||
export const CpuMetricsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"CpuMetrics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"systemMetricsCpu"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"percentTotal"}},{"kind":"Field","name":{"kind":"Name","value":"cpus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"percentTotal"}},{"kind":"Field","name":{"kind":"Name","value":"percentUser"}},{"kind":"Field","name":{"kind":"Name","value":"percentSystem"}}]}}]}}]}}]} as unknown as DocumentNode<CpuMetricsSubscription, CpuMetricsSubscriptionVariables>;
|
||||
export const LogFilesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"LogFiles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"logFiles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"size"}},{"kind":"Field","name":{"kind":"Name","value":"modifiedAt"}}]}}]}}]} as unknown as DocumentNode<LogFilesQuery, LogFilesQueryVariables>;
|
||||
export const LogFileContentDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"LogFileContent"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"path"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"lines"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"startLine"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"logFile"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"path"},"value":{"kind":"Variable","name":{"kind":"Name","value":"path"}}},{"kind":"Argument","name":{"kind":"Name","value":"lines"},"value":{"kind":"Variable","name":{"kind":"Name","value":"lines"}}},{"kind":"Argument","name":{"kind":"Name","value":"startLine"},"value":{"kind":"Variable","name":{"kind":"Name","value":"startLine"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"content"}},{"kind":"Field","name":{"kind":"Name","value":"totalLines"}},{"kind":"Field","name":{"kind":"Name","value":"startLine"}}]}}]}}]} as unknown as DocumentNode<LogFileContentQuery, LogFileContentQueryVariables>;
|
||||
export const LogFileSubscriptionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"LogFileSubscription"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"path"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"logFile"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"path"},"value":{"kind":"Variable","name":{"kind":"Name","value":"path"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"content"}},{"kind":"Field","name":{"kind":"Name","value":"totalLines"}}]}}]}}]} as unknown as DocumentNode<LogFileSubscriptionSubscription, LogFileSubscriptionSubscriptionVariables>;
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './fragment-masking';
|
||||
export * from './gql';
|
||||
export * from "./fragment-masking";
|
||||
export * from "./gql";
|
||||
58
web/src/composables/useConfirm.ts
Normal file
58
web/src/composables/useConfirm.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { ref, shallowRef } from 'vue';
|
||||
|
||||
export interface ConfirmOptions {
|
||||
title: string;
|
||||
description?: string;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
confirmVariant?: 'primary' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
|
||||
}
|
||||
|
||||
interface ConfirmState extends ConfirmOptions {
|
||||
resolve: (value: boolean) => void;
|
||||
}
|
||||
|
||||
const isOpen = ref(false);
|
||||
const state = shallowRef<ConfirmState | null>(null);
|
||||
|
||||
export function useConfirm() {
|
||||
const confirm = (options: ConfirmOptions): Promise<boolean> => {
|
||||
// Resolve any existing dialog promise with false before opening a new one
|
||||
if (state.value?.resolve) {
|
||||
const previousResolve = state.value.resolve;
|
||||
previousResolve(false);
|
||||
state.value = null;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
state.value = {
|
||||
...options,
|
||||
confirmText: options.confirmText ?? 'Confirm',
|
||||
cancelText: options.cancelText ?? 'Cancel',
|
||||
confirmVariant: options.confirmVariant ?? 'primary',
|
||||
resolve,
|
||||
};
|
||||
isOpen.value = true;
|
||||
});
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
state.value?.resolve(true);
|
||||
isOpen.value = false;
|
||||
state.value = null;
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
state.value?.resolve(false);
|
||||
isOpen.value = false;
|
||||
state.value = null;
|
||||
};
|
||||
|
||||
return {
|
||||
confirm,
|
||||
isOpen,
|
||||
state,
|
||||
handleConfirm,
|
||||
handleCancel,
|
||||
};
|
||||
}
|
||||
@@ -10,16 +10,16 @@ import AES from 'crypto-js/aes';
|
||||
|
||||
import type { SendPayloads } from '@unraid/shared-callbacks';
|
||||
|
||||
import WelcomeModalCe from '~/components/Activation/WelcomeModal.ce.vue';
|
||||
import ConnectSettingsCe from '~/components/ConnectSettings/ConnectSettings.ce.vue';
|
||||
import DowngradeOsCe from '~/components/DowngradeOs.ce.vue';
|
||||
import HeaderOsVersionCe from '~/components/HeaderOsVersion.ce.vue';
|
||||
import LogViewerCe from '~/components/Logs/LogViewer.ce.vue';
|
||||
import ModalsCe from '~/components/Modals.ce.vue';
|
||||
import RegistrationCe from '~/components/Registration.ce.vue';
|
||||
import SsoButtonCe from '~/components/SsoButton.ce.vue';
|
||||
import UpdateOsCe from '~/components/UpdateOs.ce.vue';
|
||||
import UserProfileCe from '~/components/UserProfile.ce.vue';
|
||||
import WelcomeModalCe from '~/components/Activation/WelcomeModal.standalone.vue';
|
||||
import ConnectSettingsCe from '~/components/ConnectSettings/ConnectSettings.standalone.vue';
|
||||
import DowngradeOsCe from '~/components/DowngradeOs.standalone.vue';
|
||||
import HeaderOsVersionCe from '~/components/HeaderOsVersion.standalone.vue';
|
||||
import LogViewerCe from '~/components/Logs/LogViewer.standalone.vue';
|
||||
import ModalsCe from '~/components/Modals.standalone.vue';
|
||||
import RegistrationCe from '~/components/Registration.standalone.vue';
|
||||
import SsoButtonCe from '~/components/SsoButton.standalone.vue';
|
||||
import UpdateOsCe from '~/components/UpdateOs.standalone.vue';
|
||||
import UserProfileCe from '~/components/UserProfile.standalone.vue';
|
||||
import { useThemeStore } from '~/store/theme';
|
||||
|
||||
const serverStore = useDummyServerStore();
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useQuery } from '@vue/apollo-composable';
|
||||
import { Button, Dialog, Input } from '@unraid/ui';
|
||||
import { SERVER_INFO_QUERY } from '~/pages/login.query';
|
||||
|
||||
import SsoButtonCe from '~/components/SsoButton.ce.vue';
|
||||
import SsoButtonCe from '~/components/SsoButton.standalone.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { result } = useQuery(SERVER_INFO_QUERY);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import ApiKeyAuthorize from '~/components/ApiKeyAuthorize.ce.vue';
|
||||
import ApiKeyAuthorize from '~/components/ApiKeyAuthorize.standalone.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
288
web/src/pages/tools/test-update-modal.vue
Normal file
288
web/src/pages/tools/test-update-modal.vue
Normal file
@@ -0,0 +1,288 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { Button, Label, Switch } from '@unraid/ui';
|
||||
import { useDummyServerStore } from '~/_data/serverState';
|
||||
|
||||
import type { ServerState, ServerUpdateOsResponse } from '~/types/server';
|
||||
|
||||
import CheckUpdateResponseModal from '~/components/UpdateOs/CheckUpdateResponseModal.vue';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import { useUpdateOsStore } from '~/store/updateOs';
|
||||
|
||||
const { t } = useI18n();
|
||||
const updateOsStore = useUpdateOsStore();
|
||||
const serverStore = useServerStore();
|
||||
const dummyServerStore = useDummyServerStore();
|
||||
|
||||
// Test scenarios
|
||||
const testScenarios = [
|
||||
{
|
||||
id: 'expired-ineligible',
|
||||
name: 'Expired key with ineligible update',
|
||||
description: 'License expired, update available but not eligible',
|
||||
serverState: 'EEXPIRED',
|
||||
updateResponse: {
|
||||
version: '7.1.0',
|
||||
name: 'Unraid 7.1.0',
|
||||
date: '2024-12-15',
|
||||
isNewer: true,
|
||||
isEligible: false,
|
||||
changelog: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
|
||||
changelogPretty:
|
||||
'## Unraid 7.1.0\n\n### New Features\n- Feature 1\n- Feature 2\n\n### Bug Fixes\n- Fix 1\n- Fix 2',
|
||||
sha256: undefined, // requires auth
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'normal-update',
|
||||
name: 'Normal update available',
|
||||
description: 'Active license with eligible update',
|
||||
serverState: 'BASIC',
|
||||
updateResponse: {
|
||||
version: '7.1.0',
|
||||
name: 'Unraid 7.1.0',
|
||||
date: '2024-12-15',
|
||||
isNewer: true,
|
||||
isEligible: true,
|
||||
changelog: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
|
||||
changelogPretty:
|
||||
'## Unraid 7.1.0\n\n### New Features\n- Feature 1\n- Feature 2\n\n### Bug Fixes\n- Fix 1\n- Fix 2',
|
||||
sha256: 'abc123def456789',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'renewal-required',
|
||||
name: 'Update requires renewal',
|
||||
description: 'License expired > 1 year, update requires renewal',
|
||||
serverState: 'STARTER',
|
||||
updateResponse: {
|
||||
version: '7.1.0',
|
||||
name: 'Unraid 7.1.0',
|
||||
date: '2024-12-15',
|
||||
isNewer: true,
|
||||
isEligible: false,
|
||||
changelog: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
|
||||
changelogPretty:
|
||||
'## Unraid 7.1.0\n\n### New Features\n- Feature 1\n- Feature 2\n\n### Bug Fixes\n- Fix 1\n- Fix 2',
|
||||
sha256: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'no-update',
|
||||
name: 'No update available',
|
||||
description: 'Already on latest version',
|
||||
serverState: 'BASIC',
|
||||
updateResponse: {
|
||||
version: '7.0.0',
|
||||
name: 'Unraid 7.0.0',
|
||||
date: '2024-01-15',
|
||||
isNewer: false,
|
||||
isEligible: true,
|
||||
changelog: 'https://docs.unraid.net/unraid-os/release-notes/7.0.0/',
|
||||
sha256: 'xyz789abc123',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'trial-update',
|
||||
name: 'Trial with update',
|
||||
description: 'Trial license with update available',
|
||||
serverState: 'TRIAL',
|
||||
updateResponse: {
|
||||
version: '7.1.0',
|
||||
name: 'Unraid 7.1.0',
|
||||
date: '2024-12-15',
|
||||
isNewer: true,
|
||||
isEligible: true,
|
||||
changelog: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
|
||||
sha256: 'def456ghi789',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'pro-auth-required',
|
||||
name: 'Pro license - auth required',
|
||||
description: 'Pro license but authentication required for download',
|
||||
serverState: 'PRO',
|
||||
updateResponse: {
|
||||
version: '7.1.0',
|
||||
name: 'Unraid 7.1.0',
|
||||
date: '2024-12-15',
|
||||
isNewer: true,
|
||||
isEligible: true,
|
||||
changelog: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
|
||||
sha256: undefined, // requires auth
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Component state
|
||||
const selectedScenario = ref('normal-update');
|
||||
const modalOpen = ref(false);
|
||||
const ignoreRelease = ref(false);
|
||||
const checkingForUpdates = ref(false);
|
||||
const ignoredReleases = ref<string[]>([]);
|
||||
|
||||
// Apply scenario
|
||||
const applyScenario = () => {
|
||||
const scenario = testScenarios.find((s) => s.id === selectedScenario.value);
|
||||
if (!scenario) return;
|
||||
|
||||
// Apply server state
|
||||
dummyServerStore.selector =
|
||||
scenario.serverState === 'EEXPIRED' || scenario.serverState === 'STARTER' ? 'default' : 'default';
|
||||
|
||||
// Set server state
|
||||
const currentTime = Date.now();
|
||||
const expiredTime = scenario.serverState === 'EEXPIRED' ? currentTime - 24 * 60 * 60 * 1000 : 0;
|
||||
const regExp =
|
||||
scenario.serverState === 'STARTER' ? currentTime - 400 * 24 * 60 * 60 * 1000 : undefined;
|
||||
|
||||
// Apply update response
|
||||
if (scenario.serverState === 'EEXPIRED') {
|
||||
serverStore.$patch({
|
||||
expireTime: expiredTime,
|
||||
state: 'EEXPIRED' as ServerState,
|
||||
regExp: undefined,
|
||||
});
|
||||
} else if (scenario.serverState === 'STARTER') {
|
||||
serverStore.$patch({
|
||||
state: 'STARTER' as ServerState,
|
||||
regExp: regExp,
|
||||
regTy: 'Starter',
|
||||
});
|
||||
} else {
|
||||
serverStore.$patch({
|
||||
state: scenario.serverState as ServerState,
|
||||
regExp: undefined,
|
||||
expireTime: scenario.serverState === 'TRIAL' ? currentTime + 7 * 24 * 60 * 60 * 1000 : 0,
|
||||
});
|
||||
}
|
||||
|
||||
serverStore.setUpdateOsResponse(scenario.updateResponse as ServerUpdateOsResponse);
|
||||
|
||||
// Apply ignored releases
|
||||
if (ignoreRelease.value && scenario.updateResponse.isNewer) {
|
||||
if (!ignoredReleases.value.includes(scenario.updateResponse.version)) {
|
||||
ignoredReleases.value.push(scenario.updateResponse.version);
|
||||
}
|
||||
} else {
|
||||
ignoredReleases.value = ignoredReleases.value.filter((v) => v !== scenario.updateResponse.version);
|
||||
}
|
||||
serverStore.$patch({ updateOsIgnoredReleases: ignoredReleases.value });
|
||||
};
|
||||
|
||||
// Watch for scenario changes
|
||||
watch([selectedScenario, ignoreRelease], () => {
|
||||
applyScenario();
|
||||
});
|
||||
|
||||
// Open modal with scenario
|
||||
const openModal = () => {
|
||||
applyScenario();
|
||||
updateOsStore.checkForUpdatesLoading = checkingForUpdates.value;
|
||||
modalOpen.value = true;
|
||||
updateOsStore.setModalOpen(true);
|
||||
};
|
||||
|
||||
// Initialize
|
||||
applyScenario();
|
||||
|
||||
const currentScenario = computed(() => testScenarios.find((s) => s.id === selectedScenario.value));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container mx-auto max-w-4xl p-6">
|
||||
<div class="rounded-lg bg-white p-6 shadow-lg dark:bg-zinc-900">
|
||||
<div class="mb-6">
|
||||
<h2 class="mb-2 text-2xl font-bold">Update Modal Test Page</h2>
|
||||
<p class="text-muted-foreground">
|
||||
Test various update scenarios for the CheckUpdateResponseModal component
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<!-- Scenario Selection -->
|
||||
<div class="space-y-4">
|
||||
<Label class="text-lg font-semibold">Select Test Scenario</Label>
|
||||
<div class="space-y-3">
|
||||
<div v-for="scenario in testScenarios" :key="scenario.id" class="flex items-start space-x-3">
|
||||
<input
|
||||
type="radio"
|
||||
:id="scenario.id"
|
||||
:value="scenario.id"
|
||||
v-model="selectedScenario"
|
||||
class="mt-1 rounded-full"
|
||||
/>
|
||||
<div class="flex-1">
|
||||
<Label :for="scenario.id" class="block cursor-pointer font-medium">
|
||||
{{ scenario.name }}
|
||||
</Label>
|
||||
<p class="text-muted-foreground mt-1 text-sm">{{ scenario.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Options -->
|
||||
<div class="space-y-4 border-t pt-4">
|
||||
<h3 class="font-semibold">Options</h3>
|
||||
|
||||
<div class="flex items-center space-x-3">
|
||||
<Switch id="ignore-release" v-model:checked="ignoreRelease" />
|
||||
<Label for="ignore-release" class="cursor-pointer">Ignore this release</Label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-3">
|
||||
<Switch id="checking-updates" v-model:checked="checkingForUpdates" />
|
||||
<Label for="checking-updates" class="cursor-pointer"
|
||||
>Show checking for updates loading state</Label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current State Display -->
|
||||
<div class="space-y-2 border-t pt-4">
|
||||
<h3 class="font-semibold">Current Scenario Details</h3>
|
||||
<div class="space-y-1 font-mono text-sm">
|
||||
<p><span class="font-semibold">Server State:</span> {{ currentScenario?.serverState }}</p>
|
||||
<p>
|
||||
<span class="font-semibold">Version:</span> {{ currentScenario?.updateResponse.version }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="font-semibold">Is Newer:</span> {{ currentScenario?.updateResponse.isNewer }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="font-semibold">Is Eligible:</span>
|
||||
{{ currentScenario?.updateResponse.isEligible }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="font-semibold">Has SHA256:</span>
|
||||
{{ !!currentScenario?.updateResponse.sha256 }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="font-semibold">Ignored Releases:</span>
|
||||
{{ ignoredReleases.join(', ') || 'None' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Open Modal Button -->
|
||||
<div class="border-t pt-4">
|
||||
<Button @click="openModal" variant="primary" class="w-full"> Open Update Modal </Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- The Modal Component -->
|
||||
<CheckUpdateResponseModal
|
||||
:open="modalOpen"
|
||||
@update:open="
|
||||
(val: boolean) => {
|
||||
modalOpen = val;
|
||||
updateOsStore.setModalOpen(val);
|
||||
}
|
||||
"
|
||||
:t="t"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -5,8 +5,8 @@ import { storeToRefs } from 'pinia';
|
||||
import { useActivationCodeDataStore } from '~/components/Activation/store/activationCodeData';
|
||||
import { useActivationCodeModalStore } from '~/components/Activation/store/activationCodeModal';
|
||||
import { useWelcomeModalDataStore } from '~/components/Activation/store/welcomeModalData';
|
||||
import WelcomeModalCe from '~/components/Activation/WelcomeModal.ce.vue';
|
||||
import ModalsCe from '~/components/Modals.ce.vue';
|
||||
import WelcomeModalCe from '~/components/Activation/WelcomeModal.standalone.vue';
|
||||
import ModalsCe from '~/components/Modals.standalone.vue';
|
||||
import { useCallbackActionsStore } from '~/store/callbackActions';
|
||||
|
||||
const welcomeModalRef = ref<InstanceType<typeof WelcomeModalCe>>();
|
||||
|
||||
@@ -1,327 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Standalone Vue Apps Test Page</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.test-section {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
h2 {
|
||||
color: #666;
|
||||
margin-top: 0;
|
||||
margin-bottom: 15px;
|
||||
font-size: 18px;
|
||||
}
|
||||
.status {
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
.status.loading {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
.status.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
.status.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
.mount-target {
|
||||
padding: 20px;
|
||||
background: #fafafa;
|
||||
border: 2px dashed #ddd;
|
||||
border-radius: 4px;
|
||||
min-height: 100px;
|
||||
position: relative;
|
||||
}
|
||||
.mount-target::before {
|
||||
content: attr(data-label);
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: 10px;
|
||||
background: white;
|
||||
padding: 0 5px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
.debug-info {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.multiple-mounts {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.test-button {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.test-button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
.test-button:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Teleport target for dropdowns and modals -->
|
||||
<div id="teleports"></div>
|
||||
|
||||
<!-- Mount point for Modals component -->
|
||||
<unraid-modals></unraid-modals>
|
||||
|
||||
<div class="container">
|
||||
<h1>🧪 Standalone Vue Apps Test Page</h1>
|
||||
<div id="status" class="status loading">Loading...</div>
|
||||
|
||||
<!-- Test Section 1: Single Mount -->
|
||||
<div class="test-section">
|
||||
<h2>Test 1: Single Component Mount</h2>
|
||||
<p>Testing single instance of HeaderOsVersion component</p>
|
||||
<div class="mount-target" data-label="HeaderOsVersion Mount">
|
||||
<unraid-header-os-version></unraid-header-os-version>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test Section 2: Multiple Mounts -->
|
||||
<div class="test-section">
|
||||
<h2>Test 2: Multiple Component Mounts (Shared Pinia Store)</h2>
|
||||
<p>Testing that multiple instances share the same Pinia store</p>
|
||||
<div class="multiple-mounts">
|
||||
<div class="mount-target" data-label="Instance 1">
|
||||
<unraid-header-os-version></unraid-header-os-version>
|
||||
</div>
|
||||
<div class="mount-target" data-label="Instance 2">
|
||||
<unraid-header-os-version></unraid-header-os-version>
|
||||
</div>
|
||||
<div class="mount-target" data-label="Instance 3">
|
||||
<unraid-header-os-version></unraid-header-os-version>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test Section 3: Dynamic Mount -->
|
||||
<div class="test-section">
|
||||
<h2>Test 3: Dynamic Component Creation</h2>
|
||||
<p>Test dynamically adding components after page load</p>
|
||||
<button class="test-button" id="addComponent">Add New Component</button>
|
||||
<button class="test-button" id="removeComponent">Remove Last Component</button>
|
||||
<button class="test-button" id="remountAll">Remount All</button>
|
||||
<div id="dynamicContainer" style="margin-top: 20px;">
|
||||
<!-- Dynamic components will be added here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test Section 4: Modal Testing -->
|
||||
<div class="test-section">
|
||||
<h2>Test 4: Modal Components</h2>
|
||||
<p>Test modal functionality</p>
|
||||
<button class="test-button" onclick="testTrialModal()">Open Trial Modal</button>
|
||||
<button class="test-button" onclick="testUpdateModal()">Open Update Modal</button>
|
||||
<button class="test-button" onclick="testApiKeyModal()">Open API Key Modal</button>
|
||||
<div style="margin-top: 10px;">
|
||||
<small>Note: Modals require proper store state to display</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Debug Info -->
|
||||
<div class="test-section">
|
||||
<h2>Debug Information</h2>
|
||||
<div class="debug-info" id="debugInfo">
|
||||
Waiting for initialization...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mock configurations for local testing -->
|
||||
<script>
|
||||
// Set GraphQL endpoint directly to API server
|
||||
// Change this to match your API server port
|
||||
window.GRAPHQL_ENDPOINT = 'http://localhost:3001/graphql';
|
||||
|
||||
// Mock webGui path for images
|
||||
window.__WEBGUI_PATH__ = '';
|
||||
|
||||
// Add some debug logging
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const status = document.getElementById('status');
|
||||
const debugInfo = document.getElementById('debugInfo');
|
||||
|
||||
// Log when scripts are loaded
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'childList') {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeName === 'SCRIPT') {
|
||||
console.log('Script loaded:', node.src || 'inline');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(document.head, { childList: true });
|
||||
observer.observe(document.body, { childList: true });
|
||||
|
||||
// Check for Vue app mounting
|
||||
let checkInterval = setInterval(() => {
|
||||
const mountedElements = document.querySelectorAll('unraid-header-os-version');
|
||||
let mountedCount = 0;
|
||||
|
||||
mountedElements.forEach(el => {
|
||||
if (el.innerHTML.trim() !== '') {
|
||||
mountedCount++;
|
||||
}
|
||||
});
|
||||
|
||||
if (mountedCount > 0) {
|
||||
status.className = 'status success';
|
||||
status.textContent = `✅ Successfully mounted ${mountedCount} component(s)`;
|
||||
|
||||
// Update debug info
|
||||
debugInfo.textContent = `
|
||||
Components Found: ${mountedElements.length}
|
||||
Components Mounted: ${mountedCount}
|
||||
Vue Apps: ${window.mountedApps ? Object.keys(window.mountedApps).length : 0}
|
||||
Pinia Store: ${window.globalPinia ? 'Initialized' : 'Not found'}
|
||||
GraphQL Endpoint: ${window.GRAPHQL_ENDPOINT || 'Not configured'}
|
||||
`.trim();
|
||||
|
||||
clearInterval(checkInterval);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
// Timeout after 10 seconds
|
||||
setTimeout(() => {
|
||||
if (checkInterval) {
|
||||
clearInterval(checkInterval);
|
||||
if (status.className === 'status loading') {
|
||||
status.className = 'status error';
|
||||
status.textContent = '❌ Failed to mount components (timeout)';
|
||||
}
|
||||
}
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
// Dynamic component controls
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
let dynamicCount = 0;
|
||||
const dynamicContainer = document.getElementById('dynamicContainer');
|
||||
|
||||
document.getElementById('addComponent').addEventListener('click', () => {
|
||||
dynamicCount++;
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'mount-target';
|
||||
wrapper.setAttribute('data-label', `Dynamic Instance ${dynamicCount}`);
|
||||
wrapper.style.marginBottom = '10px';
|
||||
wrapper.innerHTML = '<unraid-header-os-version></unraid-header-os-version>';
|
||||
dynamicContainer.appendChild(wrapper);
|
||||
|
||||
// Trigger mount if app is already loaded
|
||||
if (window.mountVueApp) {
|
||||
window.mountVueApp({
|
||||
component: window.HeaderOsVersion,
|
||||
selector: 'unraid-header-os-version',
|
||||
appId: `dynamic-${dynamicCount}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('removeComponent').addEventListener('click', () => {
|
||||
const lastChild = dynamicContainer.lastElementChild;
|
||||
if (lastChild) {
|
||||
dynamicContainer.removeChild(lastChild);
|
||||
dynamicCount = Math.max(0, dynamicCount - 1);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('remountAll').addEventListener('click', () => {
|
||||
// This would require the mount function to be exposed globally
|
||||
console.log('Remounting all components...');
|
||||
location.reload();
|
||||
});
|
||||
});
|
||||
|
||||
// Modal test functions
|
||||
window.testTrialModal = function() {
|
||||
console.log('Testing trial modal...');
|
||||
if (window.globalPinia) {
|
||||
const trialStore = window.globalPinia._s.get('trial');
|
||||
if (trialStore) {
|
||||
trialStore.trialModalVisible = true;
|
||||
console.log('Trial modal triggered');
|
||||
} else {
|
||||
console.error('Trial store not found');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.testUpdateModal = function() {
|
||||
console.log('Testing update modal...');
|
||||
if (window.globalPinia) {
|
||||
const updateStore = window.globalPinia._s.get('updateOs');
|
||||
if (updateStore) {
|
||||
updateStore.updateOsModalVisible = true;
|
||||
console.log('Update modal triggered');
|
||||
} else {
|
||||
console.error('Update store not found');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.testApiKeyModal = function() {
|
||||
console.log('Testing API key modal...');
|
||||
if (window.globalPinia) {
|
||||
const apiKeyStore = window.globalPinia._s.get('apiKey');
|
||||
if (apiKeyStore) {
|
||||
apiKeyStore.showCreateModal = true;
|
||||
console.log('API key modal triggered');
|
||||
} else {
|
||||
console.error('API key store not found');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Load the standalone app -->
|
||||
<script type="module" src=".nuxt/standalone-apps/standalone-apps.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -129,6 +129,10 @@ export default defineConfig({
|
||||
terserOptions: sharedTerserOptions,
|
||||
},
|
||||
|
||||
optimizeDeps: {
|
||||
include: ['ajv', 'ajv-errors', 'ajv-formats'],
|
||||
},
|
||||
|
||||
server: {
|
||||
port: 3000,
|
||||
proxy: {
|
||||
|
||||
Reference in New Issue
Block a user