mirror of
https://github.com/unraid/api.git
synced 2026-01-05 16:09:49 -06:00
Compare commits
1 Commits
v4.29.2
...
feat/cpu-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fede1b3f5 |
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "4.19.1",
|
||||
"version": "4.22.0",
|
||||
"extraOrigins": [],
|
||||
"sandbox": true,
|
||||
"ssoSubIds": [],
|
||||
|
||||
@@ -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),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
38
pnpm-lock.yaml
generated
38
pnpm-lock.yaml
generated
@@ -1103,6 +1103,9 @@ importers:
|
||||
ansi_up:
|
||||
specifier: 6.0.6
|
||||
version: 6.0.6
|
||||
chart.js:
|
||||
specifier: ^4.5.0
|
||||
version: 4.5.0
|
||||
class-variance-authority:
|
||||
specifier: 0.7.1
|
||||
version: 0.7.1
|
||||
@@ -1157,6 +1160,9 @@ importers:
|
||||
tailwind-merge:
|
||||
specifier: 2.6.0
|
||||
version: 2.6.0
|
||||
vue-chartjs:
|
||||
specifier: ^5.3.2
|
||||
version: 5.3.2(chart.js@4.5.0)(vue@3.5.20(typescript@5.9.2))
|
||||
vue-i18n:
|
||||
specifier: 11.1.11
|
||||
version: 11.1.11(vue@3.5.20(typescript@5.9.2))
|
||||
@@ -3309,6 +3315,9 @@ packages:
|
||||
'@keyv/serialize@1.1.0':
|
||||
resolution: {integrity: sha512-RlDgexML7Z63Q8BSaqhXdCYNBy/JQnqYIwxofUrNLGCblOMHp+xux2Q8nLMLlPpgHQPoU0Do8Z6btCpRBEqZ8g==}
|
||||
|
||||
'@kurkle/color@0.3.4':
|
||||
resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==}
|
||||
|
||||
'@kwsites/file-exists@1.1.1':
|
||||
resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==}
|
||||
|
||||
@@ -6301,6 +6310,10 @@ packages:
|
||||
charm@0.1.2:
|
||||
resolution: {integrity: sha512-syedaZ9cPe7r3hoQA9twWYKu5AIyCswN5+szkmPBe9ccdLrj4bYaCnLVPTLd2kgVRc7+zoX4tyPgRnFKCj5YjQ==}
|
||||
|
||||
chart.js@4.5.0:
|
||||
resolution: {integrity: sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==}
|
||||
engines: {pnpm: '>=8'}
|
||||
|
||||
check-error@2.1.1:
|
||||
resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
|
||||
engines: {node: '>= 16'}
|
||||
@@ -12449,6 +12462,12 @@ packages:
|
||||
vue-bundle-renderer@2.1.2:
|
||||
resolution: {integrity: sha512-M4WRBO/O/7G9phGaGH9AOwOnYtY9ZpPoDVpBpRzR2jO5rFL9mgIlQIgums2ljCTC2HL1jDXFQc//CzWcAQHgAw==}
|
||||
|
||||
vue-chartjs@5.3.2:
|
||||
resolution: {integrity: sha512-NrkbRRoYshbXbWqJkTN6InoDVwVb90C0R7eAVgMWcB9dPikbruaOoTFjFYHE/+tNPdIe6qdLCDjfjPHQ0fw4jw==}
|
||||
peerDependencies:
|
||||
chart.js: ^4.1.1
|
||||
vue: ^3.0.0-0 || ^2.7.0
|
||||
|
||||
vue-component-meta@2.2.8:
|
||||
resolution: {integrity: sha512-fgcP61P45AA1DacW+/532mivO5j48EEpmI7To8PK3gCVgL023QuEAPzfTA9hB6lW2hgdqiMf4gLON972pYC2+g==}
|
||||
peerDependencies:
|
||||
@@ -15172,6 +15191,8 @@ snapshots:
|
||||
|
||||
'@keyv/serialize@1.1.0': {}
|
||||
|
||||
'@kurkle/color@0.3.4': {}
|
||||
|
||||
'@kwsites/file-exists@1.1.1':
|
||||
dependencies:
|
||||
debug: 4.4.1(supports-color@5.5.0)
|
||||
@@ -15655,14 +15676,14 @@ snapshots:
|
||||
jiti: 2.5.1
|
||||
klona: 2.0.6
|
||||
knitwork: 1.2.0
|
||||
mlly: 1.7.4
|
||||
mlly: 1.8.0
|
||||
ohash: 2.0.11
|
||||
pathe: 2.0.3
|
||||
pkg-types: 2.3.0
|
||||
scule: 1.3.0
|
||||
semver: 7.7.2
|
||||
std-env: 3.9.0
|
||||
tinyglobby: 0.2.14
|
||||
tinyglobby: 0.2.15
|
||||
ufo: 1.6.1
|
||||
unctx: 2.4.1
|
||||
unimport: 5.2.0
|
||||
@@ -18637,6 +18658,10 @@ snapshots:
|
||||
|
||||
charm@0.1.2: {}
|
||||
|
||||
chart.js@4.5.0:
|
||||
dependencies:
|
||||
'@kurkle/color': 0.3.4
|
||||
|
||||
check-error@2.1.1: {}
|
||||
|
||||
chokidar@3.6.0:
|
||||
@@ -21693,7 +21718,7 @@ snapshots:
|
||||
|
||||
local-pkg@1.1.1:
|
||||
dependencies:
|
||||
mlly: 1.7.4
|
||||
mlly: 1.8.0
|
||||
pkg-types: 2.3.0
|
||||
quansync: 0.2.10
|
||||
|
||||
@@ -25284,7 +25309,7 @@ snapshots:
|
||||
picomatch: 4.0.3
|
||||
strip-ansi: 7.1.0
|
||||
tiny-invariant: 1.3.3
|
||||
tinyglobby: 0.2.14
|
||||
tinyglobby: 0.2.15
|
||||
vite: 7.1.5(@types/node@22.18.0)(jiti@2.5.1)(lightningcss@1.30.1)(stylus@0.57.0)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)
|
||||
vscode-uri: 3.1.0
|
||||
optionalDependencies:
|
||||
@@ -25515,6 +25540,11 @@ snapshots:
|
||||
dependencies:
|
||||
ufo: 1.6.1
|
||||
|
||||
vue-chartjs@5.3.2(chart.js@4.5.0)(vue@3.5.20(typescript@5.9.2)):
|
||||
dependencies:
|
||||
chart.js: 4.5.0
|
||||
vue: 3.5.20(typescript@5.9.2)
|
||||
|
||||
vue-component-meta@2.2.8(typescript@5.9.2):
|
||||
dependencies:
|
||||
'@volar/typescript': 2.4.22
|
||||
|
||||
@@ -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 {
|
||||
|
||||
1
web/components.d.ts
vendored
1
web/components.d.ts
vendored
@@ -34,6 +34,7 @@ declare module 'vue' {
|
||||
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']
|
||||
|
||||
@@ -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">
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
@@ -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,
|
||||
|
||||
@@ -153,4 +153,9 @@ export const componentMappings: ComponentMapping[] = [
|
||||
selector: 'unraid-test-theme-switcher',
|
||||
appId: 'test-theme-switcher',
|
||||
},
|
||||
{
|
||||
loader: () => import('../CpuStats/CpuStats.standalone.vue'),
|
||||
selector: 'unraid-cpu-stats',
|
||||
appId: 'cpu-stats',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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";
|
||||
@@ -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>
|
||||
@@ -1,62 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Update Modal Test - Unraid Component Test</title>
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||
<style>
|
||||
html {
|
||||
font-size: 10px;
|
||||
}
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: #f3f4f6;
|
||||
}
|
||||
.header {
|
||||
background: #1f2937;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
}
|
||||
.back-link {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
margin-bottom: 10px;
|
||||
display: inline-block;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.back-link:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div>
|
||||
<a href="/test-pages/" class="back-link">← Back to Test Pages</a>
|
||||
<h1>🧪 Update Modal Test Scenarios</h1>
|
||||
</div>
|
||||
<div>
|
||||
<unraid-test-theme-switcher></unraid-test-theme-switcher>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mount the test component -->
|
||||
<unraid-test-update-modal></unraid-test-update-modal>
|
||||
|
||||
<!-- Mount the modals component which includes the changelog modal -->
|
||||
<unraid-modals></unraid-modals>
|
||||
|
||||
<!-- Load the manifest and inject resources -->
|
||||
<script src="/test-pages/load-manifest.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