mirror of
https://github.com/unraid/api.git
synced 2026-01-02 14:40:01 -06:00
feat: mount vue apps, not web components
This commit is contained in:
@@ -1798,6 +1798,8 @@ type Server implements Node {
|
||||
guid: String!
|
||||
apikey: String!
|
||||
name: String!
|
||||
|
||||
"""Whether this server is online or offline"""
|
||||
status: ServerStatus!
|
||||
wanip: String!
|
||||
lanip: String!
|
||||
|
||||
@@ -8,6 +8,7 @@ class WebComponentsExtractor
|
||||
private const RICH_COMPONENTS_ENTRY_JS = 'unraid-components.client.js';
|
||||
private const UI_ENTRY = 'src/register.ts';
|
||||
private const UI_STYLES_ENTRY = 'style.css';
|
||||
private const STANDALONE_APPS_ENTRY = 'standalone-apps.js';
|
||||
|
||||
private static ?WebComponentsExtractor $instance = null;
|
||||
|
||||
@@ -98,6 +99,41 @@ class WebComponentsExtractor
|
||||
</script>';
|
||||
}
|
||||
|
||||
private function getStandaloneAppsScript(): string
|
||||
{
|
||||
$manifestFiles = $this->findManifestFiles('standalone.manifest.json');
|
||||
|
||||
if (empty($manifestFiles)) {
|
||||
// No standalone apps, return empty
|
||||
return '';
|
||||
}
|
||||
|
||||
$manifestPath = $manifestFiles[0];
|
||||
$manifest = $this->getManifestContents($manifestPath);
|
||||
$subfolder = $this->getRelativePath($manifestPath);
|
||||
|
||||
if (!isset($manifest[self::STANDALONE_APPS_ENTRY])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$jsFile = ($subfolder ? $subfolder . '/' : '') . $manifest[self::STANDALONE_APPS_ENTRY]['file'];
|
||||
|
||||
// Use a unique identifier to prevent duplicate script loading
|
||||
$scriptId = 'unraid-standalone-apps-script';
|
||||
return '<script id="' . $scriptId . '" type="module" src="' . $this->getAssetPath($jsFile) . '"></script>
|
||||
<script>
|
||||
// Remove duplicate script tags to prevent multiple loads
|
||||
(function() {
|
||||
var scripts = document.querySelectorAll(\'script[id="' . $scriptId . '"]\');
|
||||
if (scripts.length > 1) {
|
||||
for (var i = 1; i < scripts.length; i++) {
|
||||
scripts[i].remove();
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>';
|
||||
}
|
||||
|
||||
private function getUnraidUiScriptHtml(): string
|
||||
{
|
||||
$manifestFiles = $this->findManifestFiles('ui.manifest.json');
|
||||
@@ -173,7 +209,9 @@ class WebComponentsExtractor
|
||||
|
||||
try {
|
||||
$scriptsOutput = true;
|
||||
return $this->getRichComponentsScript() . $this->getUnraidUiScriptHtml();
|
||||
return $this->getRichComponentsScript() .
|
||||
$this->getUnraidUiScriptHtml() .
|
||||
$this->getStandaloneAppsScript();
|
||||
} catch (\Exception $e) {
|
||||
error_log("Error in WebComponentsExtractor::getScriptTagHtml: " . $e->getMessage());
|
||||
$scriptsOutput = false; // Reset on error
|
||||
|
||||
199
web/components/Wrapper/vue-mount-app.ts
Normal file
199
web/components/Wrapper/vue-mount-app.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import { createApp } from 'vue';
|
||||
import type { App as VueApp, Component } from 'vue';
|
||||
import { createI18n } from 'vue-i18n';
|
||||
import { DefaultApolloClient } from '@vue/apollo-composable';
|
||||
|
||||
// Import Tailwind CSS for injection
|
||||
import tailwindStyles from '~/assets/main.css?inline';
|
||||
|
||||
import en_US from '~/locales/en_US.json';
|
||||
import { createHtmlEntityDecoder } from '~/helpers/i18n-utils';
|
||||
import { globalPinia } from '~/store/globalPinia';
|
||||
import { client } from '~/helpers/create-apollo-client';
|
||||
|
||||
// Global store for mounted apps
|
||||
const mountedApps = new Map<string, VueApp>();
|
||||
|
||||
// Shared style injection tracking
|
||||
const styleInjected = new WeakSet<Document | ShadowRoot>();
|
||||
|
||||
// Expose globally for debugging
|
||||
declare global {
|
||||
interface Window {
|
||||
mountedApps: Map<string, VueApp>;
|
||||
globalPinia: typeof globalPinia;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.mountedApps = mountedApps;
|
||||
window.globalPinia = globalPinia;
|
||||
}
|
||||
|
||||
function injectStyles(root: Document | ShadowRoot) {
|
||||
// Always inject to document for teleported elements
|
||||
if (!styleInjected.has(document)) {
|
||||
const globalStyleElement = document.createElement('style');
|
||||
globalStyleElement.setAttribute('data-tailwind-global', 'true');
|
||||
globalStyleElement.textContent = tailwindStyles;
|
||||
document.head.appendChild(globalStyleElement);
|
||||
styleInjected.add(document);
|
||||
}
|
||||
|
||||
// Also inject to shadow root if needed
|
||||
if (root !== document && !styleInjected.has(root)) {
|
||||
const styleElement = document.createElement('style');
|
||||
styleElement.setAttribute('data-tailwind', 'true');
|
||||
styleElement.textContent = tailwindStyles;
|
||||
root.appendChild(styleElement);
|
||||
styleInjected.add(root);
|
||||
}
|
||||
}
|
||||
|
||||
function setupI18n() {
|
||||
const defaultLocale = 'en_US';
|
||||
let parsedLocale = '';
|
||||
let parsedMessages = {};
|
||||
let nonDefaultLocale = false;
|
||||
|
||||
// Check for window locale data
|
||||
if (typeof window !== 'undefined') {
|
||||
const windowLocaleData = (window as unknown as { LOCALE_DATA?: string }).LOCALE_DATA || null;
|
||||
if (windowLocaleData) {
|
||||
try {
|
||||
parsedMessages = JSON.parse(decodeURIComponent(windowLocaleData));
|
||||
parsedLocale = Object.keys(parsedMessages)[0];
|
||||
nonDefaultLocale = parsedLocale !== defaultLocale;
|
||||
} catch (error) {
|
||||
console.error('[VueMountApp] error parsing messages', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return createI18n({
|
||||
legacy: false,
|
||||
locale: nonDefaultLocale ? parsedLocale : defaultLocale,
|
||||
fallbackLocale: defaultLocale,
|
||||
messages: {
|
||||
en_US,
|
||||
...(nonDefaultLocale ? parsedMessages : {}),
|
||||
},
|
||||
postTranslation: createHtmlEntityDecoder(),
|
||||
});
|
||||
}
|
||||
|
||||
export interface MountOptions {
|
||||
component: Component;
|
||||
selector: string;
|
||||
appId?: string;
|
||||
useShadowRoot?: boolean;
|
||||
props?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export function mountVueApp(options: MountOptions): VueApp | null {
|
||||
const { component, selector, appId = selector, useShadowRoot = false, props = {} } = options;
|
||||
|
||||
// Check if app is already mounted
|
||||
if (mountedApps.has(appId)) {
|
||||
console.warn(`[VueMountApp] App ${appId} is already mounted`);
|
||||
return mountedApps.get(appId)!;
|
||||
}
|
||||
|
||||
// Find all mount targets
|
||||
const targets = document.querySelectorAll(selector);
|
||||
if (targets.length === 0) {
|
||||
console.warn(`[VueMountApp] No elements found for selector: ${selector}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create the Vue app
|
||||
const app = createApp(component, props);
|
||||
|
||||
// Setup i18n
|
||||
const i18n = setupI18n();
|
||||
app.use(i18n);
|
||||
|
||||
// Use the shared Pinia instance
|
||||
app.use(globalPinia);
|
||||
|
||||
// Provide Apollo client
|
||||
app.provide(DefaultApolloClient, client);
|
||||
|
||||
// Mount to all targets
|
||||
targets.forEach((target) => {
|
||||
const mountTarget = target as HTMLElement;
|
||||
|
||||
if (useShadowRoot) {
|
||||
// Create shadow root if needed
|
||||
if (!mountTarget.shadowRoot) {
|
||||
mountTarget.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
// Create mount container in shadow root
|
||||
const container = document.createElement('div');
|
||||
container.id = 'app';
|
||||
mountTarget.shadowRoot!.appendChild(container);
|
||||
|
||||
// Inject styles into shadow root
|
||||
injectStyles(mountTarget.shadowRoot!);
|
||||
|
||||
// Clone and mount the app to this container
|
||||
const clonedApp = createApp(component, props);
|
||||
clonedApp.use(i18n);
|
||||
clonedApp.use(globalPinia);
|
||||
clonedApp.provide(DefaultApolloClient, client);
|
||||
clonedApp.mount(container);
|
||||
} else {
|
||||
// Direct mount without shadow root
|
||||
injectStyles(document);
|
||||
|
||||
// For multiple targets, we need to create separate app instances
|
||||
// but they'll share the same Pinia store
|
||||
if (Array.from(targets).indexOf(mountTarget) === 0) {
|
||||
// First target, use the main app
|
||||
app.mount(mountTarget);
|
||||
} else {
|
||||
// Additional targets, create cloned apps
|
||||
const clonedApp = createApp(component, props);
|
||||
clonedApp.use(i18n);
|
||||
clonedApp.use(globalPinia); // Shared Pinia instance
|
||||
clonedApp.provide(DefaultApolloClient, client);
|
||||
clonedApp.mount(mountTarget);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Store the app reference
|
||||
mountedApps.set(appId, app);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
export function unmountVueApp(appId: string): boolean {
|
||||
const app = mountedApps.get(appId);
|
||||
if (!app) {
|
||||
console.warn(`[VueMountApp] No app found with id: ${appId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
app.unmount();
|
||||
mountedApps.delete(appId);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function getMountedApp(appId: string): VueApp | undefined {
|
||||
return mountedApps.get(appId);
|
||||
}
|
||||
|
||||
// Auto-mount function for script tags
|
||||
export function autoMountComponent(component: Component, selector: string, options?: Partial<MountOptions>) {
|
||||
// Wait for DOM to be ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
mountVueApp({ component, selector, ...options });
|
||||
});
|
||||
} else {
|
||||
// DOM is already ready
|
||||
mountVueApp({ component, selector, ...options });
|
||||
}
|
||||
}
|
||||
123
web/components/standalone-mount.ts
Normal file
123
web/components/standalone-mount.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
// Import all components
|
||||
import type { Component } from 'vue';
|
||||
import Auth from './Auth.ce.vue';
|
||||
import ConnectSettings from './ConnectSettings/ConnectSettings.ce.vue';
|
||||
import DownloadApiLogs from './DownloadApiLogs.ce.vue';
|
||||
import HeaderOsVersion from './HeaderOsVersion.ce.vue';
|
||||
import Modals from './Modals.ce.vue';
|
||||
import UserProfile from './UserProfile.ce.vue';
|
||||
import UpdateOs from './UpdateOs.ce.vue';
|
||||
import DowngradeOs from './DowngradeOs.ce.vue';
|
||||
import Registration from './Registration.ce.vue';
|
||||
import WanIpCheck from './WanIpCheck.ce.vue';
|
||||
import WelcomeModal from './Activation/WelcomeModal.ce.vue';
|
||||
import SsoButton from './SsoButton.ce.vue';
|
||||
import LogViewer from './Logs/LogViewer.ce.vue';
|
||||
import ThemeSwitcher from './ThemeSwitcher.ce.vue';
|
||||
import ApiKeyPage from './ApiKeyPage.ce.vue';
|
||||
import DevModalTest from './DevModalTest.ce.vue';
|
||||
import ApiKeyAuthorize from './ApiKeyAuthorize.ce.vue';
|
||||
|
||||
// Import utilities
|
||||
import { autoMountComponent, mountVueApp, getMountedApp } from './Wrapper/vue-mount-app';
|
||||
import { useThemeStore } from '~/store/theme';
|
||||
import { globalPinia } from '~/store/globalPinia';
|
||||
|
||||
// Initialize theme store and set CSS variables
|
||||
if (typeof window !== 'undefined') {
|
||||
// Ensure pinia is ready
|
||||
const themeStore = useThemeStore(globalPinia);
|
||||
themeStore.setTheme();
|
||||
themeStore.setCssVars();
|
||||
}
|
||||
|
||||
// Define component mappings
|
||||
const componentMappings = [
|
||||
{ component: Auth, selector: 'unraid-auth', appId: 'auth' },
|
||||
{ component: ConnectSettings, selector: 'unraid-connect-settings', appId: 'connect-settings' },
|
||||
{ component: DownloadApiLogs, selector: 'unraid-download-api-logs', appId: 'download-api-logs' },
|
||||
{ component: HeaderOsVersion, selector: 'unraid-header-os-version', appId: 'header-os-version' },
|
||||
{ component: Modals, selector: 'unraid-modals', appId: 'modals' },
|
||||
{ component: UserProfile, selector: 'unraid-user-profile', appId: 'user-profile' },
|
||||
{ component: UpdateOs, selector: 'unraid-update-os', appId: 'update-os' },
|
||||
{ component: DowngradeOs, selector: 'unraid-downgrade-os', appId: 'downgrade-os' },
|
||||
{ component: Registration, selector: 'unraid-registration', appId: 'registration' },
|
||||
{ component: WanIpCheck, selector: 'unraid-wan-ip-check', appId: 'wan-ip-check' },
|
||||
{ component: WelcomeModal, selector: 'unraid-welcome-modal', appId: 'welcome-modal' },
|
||||
{ component: SsoButton, selector: 'unraid-sso-button', appId: 'sso-button' },
|
||||
{ component: LogViewer, selector: 'unraid-log-viewer', appId: 'log-viewer' },
|
||||
{ component: ThemeSwitcher, selector: 'unraid-theme-switcher', appId: 'theme-switcher' },
|
||||
{ component: ApiKeyPage, selector: 'unraid-api-key-manager', appId: 'api-key-manager' },
|
||||
{ component: DevModalTest, selector: 'unraid-dev-modal-test', appId: 'dev-modal-test' },
|
||||
{ component: ApiKeyAuthorize, selector: 'unraid-api-key-authorize', appId: 'api-key-authorize' },
|
||||
];
|
||||
|
||||
// Auto-mount all components
|
||||
componentMappings.forEach(({ component, selector, appId }) => {
|
||||
autoMountComponent(component, selector, {
|
||||
appId,
|
||||
useShadowRoot: false, // Mount directly to avoid shadow DOM issues
|
||||
});
|
||||
});
|
||||
|
||||
// Special handling for Modals - also mount to #modals if it exists
|
||||
if (document.querySelector('#modals')) {
|
||||
mountVueApp({
|
||||
component: Modals,
|
||||
selector: '#modals',
|
||||
appId: 'modals-direct',
|
||||
useShadowRoot: false,
|
||||
});
|
||||
}
|
||||
|
||||
// Expose functions globally for testing and dynamic mounting
|
||||
declare global {
|
||||
interface Window {
|
||||
UnraidComponents: Record<string, Component>;
|
||||
mountVueApp: typeof mountVueApp;
|
||||
getMountedApp: typeof getMountedApp;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
// Expose all components
|
||||
window.UnraidComponents = {
|
||||
Auth,
|
||||
ConnectSettings,
|
||||
DownloadApiLogs,
|
||||
HeaderOsVersion,
|
||||
Modals,
|
||||
UserProfile,
|
||||
UpdateOs,
|
||||
DowngradeOs,
|
||||
Registration,
|
||||
WanIpCheck,
|
||||
WelcomeModal,
|
||||
SsoButton,
|
||||
LogViewer,
|
||||
ThemeSwitcher,
|
||||
ApiKeyPage,
|
||||
DevModalTest,
|
||||
ApiKeyAuthorize,
|
||||
};
|
||||
|
||||
// Expose utility functions
|
||||
window.mountVueApp = mountVueApp;
|
||||
window.getMountedApp = getMountedApp;
|
||||
|
||||
// Create dynamic mount functions for each component
|
||||
componentMappings.forEach(({ component, selector, appId }) => {
|
||||
const componentName = appId.split('-').map(word =>
|
||||
word.charAt(0).toUpperCase() + word.slice(1)
|
||||
).join('');
|
||||
|
||||
(window as unknown as Record<string, unknown>)[`mount${componentName}`] = (customSelector?: string) => {
|
||||
return mountVueApp({
|
||||
component,
|
||||
selector: customSelector || selector,
|
||||
appId: `${appId}-${Date.now()}`,
|
||||
useShadowRoot: false,
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -4,11 +4,29 @@ import { RetryLink } from '@apollo/client/link/retry/index.js';
|
||||
import { GraphQLWsLink } from '@apollo/client/link/subscriptions/index.js';
|
||||
import { getMainDefinition } from '@apollo/client/utilities/index.js';
|
||||
import { createClient } from 'graphql-ws';
|
||||
|
||||
import type { ErrorResponse } from '@apollo/client/link/error/index.js';
|
||||
import type { GraphQLFormattedError } from 'graphql';
|
||||
|
||||
import { createApolloCache } from './apollo-cache';
|
||||
import { WEBGUI_GRAPHQL } from './urls';
|
||||
|
||||
const httpEndpoint = WEBGUI_GRAPHQL;
|
||||
const wsEndpoint = new URL(WEBGUI_GRAPHQL.toString().replace('http', 'ws'));
|
||||
// Allow overriding the GraphQL endpoint for development/testing
|
||||
declare global {
|
||||
interface Window {
|
||||
GRAPHQL_ENDPOINT?: string;
|
||||
}
|
||||
}
|
||||
|
||||
const getGraphQLEndpoint = () => {
|
||||
if (typeof window !== 'undefined' && window.GRAPHQL_ENDPOINT) {
|
||||
return new URL(window.GRAPHQL_ENDPOINT);
|
||||
}
|
||||
return WEBGUI_GRAPHQL;
|
||||
};
|
||||
|
||||
const httpEndpoint = getGraphQLEndpoint();
|
||||
const wsEndpoint = new URL(httpEndpoint.toString().replace('http', 'ws'));
|
||||
const DEV_MODE = (globalThis as unknown as { __DEV__: boolean }).__DEV__ ?? false;
|
||||
|
||||
const headers = {
|
||||
@@ -28,17 +46,15 @@ const wsLink = new GraphQLWsLink(
|
||||
})
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const errorLink = onError(({ graphQLErrors, networkError }: any) => {
|
||||
const errorLink = onError(({ graphQLErrors, networkError }: ErrorResponse) => {
|
||||
if (graphQLErrors) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
graphQLErrors.map((error: any) => {
|
||||
graphQLErrors.forEach((error: GraphQLFormattedError) => {
|
||||
console.error('[GraphQL error]', error);
|
||||
const errorMsg = error.error?.message ?? error.message;
|
||||
const errorMsg =
|
||||
(error as GraphQLFormattedError & { error?: { message?: string } }).error?.message ?? error.message;
|
||||
if (errorMsg?.includes('offline')) {
|
||||
// @todo restart the api, but make sure not to trigger infinite loop
|
||||
}
|
||||
return error.message;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -46,9 +62,8 @@ const errorLink = onError(({ graphQLErrors, networkError }: any) => {
|
||||
console.error(`[Network error]: ${networkError}`);
|
||||
const msg = networkError.message ? networkError.message : networkError;
|
||||
if (typeof msg === 'string' && msg.includes('Unexpected token < in JSON at position 0')) {
|
||||
return 'Unraid API • CORS Error';
|
||||
console.error('Unraid API • CORS Error');
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -69,7 +84,7 @@ const retryLink = new RetryLink({
|
||||
// Disable Apollo Client if not in DEV Mode and server state says unraid-api is not running
|
||||
const disableQueryLink = new ApolloLink((operation, forward) => {
|
||||
if (!DEV_MODE && operation.getContext().serverState?.unraidApi?.status === 'offline') {
|
||||
return null;
|
||||
return null;
|
||||
}
|
||||
return forward(operation);
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import path from 'path';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import removeConsole from 'vite-plugin-remove-console';
|
||||
|
||||
import type { PluginOption, UserConfig } from 'vite';
|
||||
import type { PluginOption } from 'vite';
|
||||
|
||||
/**
|
||||
* Used to avoid redeclaring variables in the webgui codebase.
|
||||
@@ -34,15 +34,13 @@ console.log(dropConsole ? 'WARN: Console logs are disabled' : 'INFO: Console log
|
||||
|
||||
const assetsDir = path.join(__dirname, '../api/dev/webGui/');
|
||||
|
||||
/**
|
||||
* Create a tag configuration
|
||||
*/
|
||||
const createWebComponentTag = (name: string, path: string, appContext: string) => ({
|
||||
async: false,
|
||||
name,
|
||||
path,
|
||||
appContext
|
||||
});
|
||||
// REMOVED: No longer needed with standalone mount approach
|
||||
// const createWebComponentTag = (name: string, path: string, appContext: string) => ({
|
||||
// async: false,
|
||||
// name,
|
||||
// path,
|
||||
// appContext
|
||||
// });
|
||||
|
||||
/**
|
||||
* Shared terser options for consistent minification
|
||||
@@ -118,26 +116,24 @@ const sharedDefine = {
|
||||
__VUE_PROD_DEVTOOLS__: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply shared Vite configuration to a config object
|
||||
*/
|
||||
const applySharedViteConfig = (config: UserConfig, includeJQueryIsolation = false) => {
|
||||
if (!config.plugins) config.plugins = [];
|
||||
if (!config.define) config.define = {};
|
||||
if (!config.build) config.build = {};
|
||||
|
||||
// Add shared plugins
|
||||
config.plugins.push(...getSharedPlugins(includeJQueryIsolation));
|
||||
|
||||
// Merge define values
|
||||
Object.assign(config.define, sharedDefine);
|
||||
|
||||
// Apply build configuration
|
||||
config.build.minify = 'terser';
|
||||
config.build.terserOptions = sharedTerserOptions;
|
||||
|
||||
return config;
|
||||
};
|
||||
// REMOVED: No longer needed with standalone mount approach
|
||||
// const applySharedViteConfig = (config: UserConfig, includeJQueryIsolation = false) => {
|
||||
// if (!config.plugins) config.plugins = [];
|
||||
// if (!config.define) config.define = {};
|
||||
// if (!config.build) config.build = {};
|
||||
//
|
||||
// // Add shared plugins
|
||||
// config.plugins.push(...getSharedPlugins(includeJQueryIsolation));
|
||||
//
|
||||
// // Merge define values
|
||||
// Object.assign(config.define, sharedDefine);
|
||||
//
|
||||
// // Apply build configuration
|
||||
// config.build.minify = 'terser';
|
||||
// config.build.terserOptions = sharedTerserOptions;
|
||||
//
|
||||
// return config;
|
||||
// };
|
||||
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
@@ -205,45 +201,11 @@ export default defineNuxtConfig({
|
||||
},
|
||||
},
|
||||
|
||||
customElements: {
|
||||
analyzer: process.env.NODE_ENV !== 'test',
|
||||
entries: [
|
||||
// @ts-expect-error The nuxt-custom-elements module types don't perfectly match our configuration object structure.
|
||||
// The custom elements configuration requires specific properties and methods that may not align with the
|
||||
// module's TypeScript definitions, particularly around the viteExtend function and tag configuration format.
|
||||
{
|
||||
name: 'UnraidComponents',
|
||||
viteExtend(config: UserConfig) {
|
||||
const sharedConfig = applySharedViteConfig(config, true);
|
||||
|
||||
// Optimize CSS while keeping it inlined for functionality
|
||||
if (!sharedConfig.css) sharedConfig.css = {};
|
||||
sharedConfig.css.devSourcemap = process.env.NODE_ENV === 'development';
|
||||
|
||||
return sharedConfig;
|
||||
},
|
||||
tags: [
|
||||
createWebComponentTag('UnraidAuth', '@/components/Auth.ce', '@/components/Wrapper/web-component-plugins'),
|
||||
createWebComponentTag('UnraidConnectSettings', '@/components/ConnectSettings/ConnectSettings.ce', '@/components/Wrapper/web-component-plugins'),
|
||||
createWebComponentTag('UnraidDownloadApiLogs', '@/components/DownloadApiLogs.ce', '@/components/Wrapper/web-component-plugins'),
|
||||
createWebComponentTag('UnraidHeaderOsVersion', '@/components/HeaderOsVersion.ce', '@/components/Wrapper/web-component-plugins'),
|
||||
createWebComponentTag('UnraidModals', '@/components/Modals.ce', '@/components/Wrapper/web-component-plugins'),
|
||||
createWebComponentTag('UnraidUserProfile', '@/components/UserProfile.ce', '@/components/Wrapper/web-component-plugins'),
|
||||
createWebComponentTag('UnraidUpdateOs', '@/components/UpdateOs.ce', '@/components/Wrapper/web-component-plugins'),
|
||||
createWebComponentTag('UnraidDowngradeOs', '@/components/DowngradeOs.ce', '@/components/Wrapper/web-component-plugins'),
|
||||
createWebComponentTag('UnraidRegistration', '@/components/Registration.ce', '@/components/Wrapper/web-component-plugins'),
|
||||
createWebComponentTag('UnraidWanIpCheck', '@/components/WanIpCheck.ce', '@/components/Wrapper/web-component-plugins'),
|
||||
createWebComponentTag('UnraidWelcomeModal', '@/components/Activation/WelcomeModal.ce', '@/components/Wrapper/web-component-plugins'),
|
||||
createWebComponentTag('UnraidSsoButton', '@/components/SsoButton.ce', '@/components/Wrapper/web-component-plugins'),
|
||||
createWebComponentTag('UnraidLogViewer', '@/components/Logs/LogViewer.ce', '@/components/Wrapper/web-component-plugins'),
|
||||
createWebComponentTag('UnraidThemeSwitcher', '@/components/ThemeSwitcher.ce', '@/components/Wrapper/web-component-plugins'),
|
||||
createWebComponentTag('UnraidApiKeyManager', '@/components/ApiKeyPage.ce', '@/components/Wrapper/web-component-plugins'),
|
||||
createWebComponentTag('UnraidDevModalTest', '@/components/DevModalTest.ce', '@/components/Wrapper/web-component-plugins'),
|
||||
createWebComponentTag('UnraidApiKeyAuthorize', '@/components/ApiKeyAuthorize.ce', '@/components/Wrapper/web-component-plugins'),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
// DISABLED: Using standalone mount approach instead
|
||||
// customElements: {
|
||||
// analyzer: process.env.NODE_ENV !== 'test',
|
||||
// entries: [],
|
||||
// },
|
||||
|
||||
compatibilityDate: '2024-12-05',
|
||||
|
||||
|
||||
@@ -13,11 +13,13 @@
|
||||
"prebuild:dev": "pnpm predev",
|
||||
"build:dev": "nuxi build --dotenv .env.production && pnpm run manifest-ts && pnpm run deploy-to-unraid:dev",
|
||||
"build:webgui": "pnpm run type-check && nuxi build --dotenv .env.production && pnpm run manifest-ts && pnpm run copy-to-webgui-repo",
|
||||
"build": "NODE_ENV=production nuxi build --dotenv .env.production && pnpm run manifest-ts && pnpm run validate:css",
|
||||
"build": "NODE_ENV=production nuxi build --dotenv .env.production && pnpm run build:standalone && pnpm run manifest-ts && pnpm run validate:css",
|
||||
"build:standalone": "vite build --config vite.standalone.config.ts && pnpm run manifest-standalone",
|
||||
"prebuild:watch": "pnpm predev",
|
||||
"build:watch": "nuxi build --dotenv .env.production --watch && pnpm run manifest-ts",
|
||||
"generate": "nuxt generate",
|
||||
"manifest-ts": "node ./scripts/add-timestamp-webcomponent-manifest.js",
|
||||
"manifest-standalone": "node ./scripts/add-timestamp-standalone-manifest.js",
|
||||
"validate:css": "node ./scripts/validate-custom-elements-css.js",
|
||||
"// Deployment": "",
|
||||
"unraid:deploy": "pnpm build:dev",
|
||||
@@ -35,6 +37,7 @@
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:ci": "vitest run",
|
||||
"test:standalone": "pnpm run build:standalone && vite --config vite.test.config.ts",
|
||||
"// Nuxt": "",
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
|
||||
1
web/public/images/UN-logotype-gradient.svg
Normal file
1
web/public/images/UN-logotype-gradient.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 222.36 39.04"><defs><linearGradient id="header-logo" x1="47.53" y1="79.1" x2="170.71" y2="-44.08" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#e32929"/><stop offset="1" stop-color="#ff8d30"/></linearGradient></defs><title>unraid.net</title><path d="M146.7,29.47H135l-3,9h-6.49L138.93,0h8l13.41,38.49h-7.09L142.62,6.93l-5.83,16.88h8ZM29.69,0V25.4c0,8.91-5.77,13.64-14.9,13.64S0,34.31,0,25.4V0H6.54V25.4c0,5.17,3.19,7.92,8.25,7.92s8.36-2.75,8.36-7.92V0ZM50.86,12v26.5H44.31V0h6.11l17,26.5V0H74V38.49H67.9ZM171.29,0h6.54V38.49h-6.54Zm51.07,24.69c0,9-5.88,13.8-15.17,13.8H192.67V0H207.3c9.18,0,15.06,4.78,15.06,13.8ZM215.82,13.8c0-5.28-3.3-8.14-8.52-8.14h-8.08V32.77h8c5.33,0,8.63-2.8,8.63-8.08ZM108.31,23.92c4.34-1.6,6.93-5.28,6.93-11.55C115.24,3.68,110.18,0,102.48,0H88.84V38.49h6.55V5.66h6.87c3.8,0,6.21,1.82,6.21,6.71s-2.41,6.76-6.21,6.76H98.88l9.21,19.36h7.53Z" fill="url(#header-logo)"/></svg>
|
||||
|
After Width: | Height: | Size: 1008 B |
29
web/scripts/add-timestamp-standalone-manifest.js
Normal file
29
web/scripts/add-timestamp-standalone-manifest.js
Normal file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const distPath = '.nuxt/standalone-apps';
|
||||
const manifestPath = path.join(distPath, 'standalone.manifest.json');
|
||||
|
||||
// Get all JS files in the dist directory
|
||||
const files = fs.readdirSync(distPath);
|
||||
const manifest = {};
|
||||
|
||||
files.forEach(file => {
|
||||
if (file.endsWith('.js') || file.endsWith('.css')) {
|
||||
const key = file.replace(/\.(js|css)$/, '.$1');
|
||||
manifest[key] = {
|
||||
file: file,
|
||||
src: file,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Add timestamp
|
||||
manifest.ts = Date.now();
|
||||
|
||||
// Write manifest
|
||||
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
||||
|
||||
console.log('Standalone apps manifest created:', manifestPath);
|
||||
@@ -2,6 +2,13 @@ const fs = require('fs');
|
||||
|
||||
// Read the JSON file
|
||||
const filePath = '../web/.nuxt/nuxt-custom-elements/dist/unraid-components/manifest.json';
|
||||
|
||||
// Check if file exists (web components are now disabled in favor of standalone)
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.log('Web components manifest not found (using standalone mount instead)');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const jsonData = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
||||
|
||||
// Add timestamp (ts) to the JSON data
|
||||
|
||||
@@ -10,8 +10,9 @@ fi
|
||||
# Set server name from command-line argument
|
||||
server_name="$1"
|
||||
|
||||
# Source directory path
|
||||
# Source directory paths
|
||||
source_directory=".nuxt/nuxt-custom-elements/dist/unraid-components/"
|
||||
standalone_directory=".nuxt/standalone-apps/"
|
||||
|
||||
if [ ! -d "$source_directory" ]; then
|
||||
echo "The web components directory does not exist."
|
||||
@@ -24,6 +25,11 @@ ssh "root@${server_name}" "rm -rf /usr/local/emhttp/plugins/dynamix.my.servers/u
|
||||
|
||||
rsync_command="rsync -avz -e ssh $source_directory root@${server_name}:/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/nuxt"
|
||||
|
||||
# Also sync standalone apps if they exist
|
||||
if [ -d "$standalone_directory" ]; then
|
||||
rsync_standalone="rsync -avz -e ssh $standalone_directory root@${server_name}:/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/standalone"
|
||||
fi
|
||||
|
||||
echo "Executing the following command:"
|
||||
echo "$rsync_command"
|
||||
|
||||
@@ -31,6 +37,13 @@ echo "$rsync_command"
|
||||
eval "$rsync_command"
|
||||
exit_code=$?
|
||||
|
||||
# Execute standalone rsync if directory exists
|
||||
if [ -n "$rsync_standalone" ]; then
|
||||
echo "Executing standalone apps sync:"
|
||||
echo "$rsync_standalone"
|
||||
eval "$rsync_standalone"
|
||||
fi
|
||||
|
||||
# Update the auth-request.php file to include the new web component JS
|
||||
update_auth_request() {
|
||||
local server_name="$1"
|
||||
@@ -38,9 +51,16 @@ update_auth_request() {
|
||||
ssh "root@${server_name}" "
|
||||
AUTH_REQUEST_FILE='/usr/local/emhttp/auth-request.php'
|
||||
WEB_COMPS_DIR='/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/nuxt/_nuxt/'
|
||||
STANDALONE_DIR='/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/standalone/'
|
||||
|
||||
# Find JS files and modify paths
|
||||
mapfile -t JS_FILES < <(find \"\$WEB_COMPS_DIR\" -type f -name \"*.js\" | sed 's|/usr/local/emhttp||' | sort -u)
|
||||
|
||||
# Find standalone JS files if directory exists
|
||||
if [ -d \"\$STANDALONE_DIR\" ]; then
|
||||
mapfile -t STANDALONE_JS < <(find \"\$STANDALONE_DIR\" -type f -name \"*.js\" | sed 's|/usr/local/emhttp||' | sort -u)
|
||||
FILES_TO_ADD+=(\"\${STANDALONE_JS[@]}\")
|
||||
fi
|
||||
|
||||
FILES_TO_ADD+=(\"\${JS_FILES[@]}\")
|
||||
|
||||
|
||||
@@ -29,16 +29,26 @@ function findJSFiles(dir, jsFiles = []) {
|
||||
* Validates that Tailwind CSS styles are properly inlined in the JavaScript bundle
|
||||
*/
|
||||
function validateCustomElementsCSS() {
|
||||
console.log('🔍 Validating custom elements JS bundle includes inlined Tailwind styles...');
|
||||
console.log('🔍 Validating JS bundle includes inlined Tailwind styles...');
|
||||
|
||||
try {
|
||||
// Find the custom elements JS files
|
||||
const customElementsDir = '.nuxt/nuxt-custom-elements/dist';
|
||||
const jsFiles = findJSFiles(customElementsDir);
|
||||
|
||||
// Check standalone apps first (new approach)
|
||||
const standaloneDir = '.nuxt/standalone-apps';
|
||||
let jsFiles = findJSFiles(standaloneDir);
|
||||
let usingStandalone = true;
|
||||
|
||||
// Fallback to custom elements if standalone doesn't exist
|
||||
if (jsFiles.length === 0) {
|
||||
throw new Error('No custom elements JS files found in ' + customElementsDir);
|
||||
const customElementsDir = '.nuxt/nuxt-custom-elements/dist';
|
||||
jsFiles = findJSFiles(customElementsDir);
|
||||
usingStandalone = false;
|
||||
|
||||
if (jsFiles.length === 0) {
|
||||
throw new Error('No JS files found in standalone apps or custom elements dist');
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`📦 Using ${usingStandalone ? 'standalone apps' : 'custom elements'} bundle`);
|
||||
|
||||
// Find the largest JS file (likely the main bundle with inlined CSS)
|
||||
const jsFile = jsFiles.reduce((largest, current) => {
|
||||
|
||||
330
web/test-standalone.html
Normal file
330
web/test-standalone.html
Normal file
@@ -0,0 +1,330 @@
|
||||
<!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 -->
|
||||
<div id="modals"></div>
|
||||
|
||||
<!-- Alternative: Use custom element -->
|
||||
<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>
|
||||
85
web/vite.standalone.config.ts
Normal file
85
web/vite.standalone.config.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import path, { resolve } from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
// Read CSS content at build time
|
||||
const getCssContent = () => {
|
||||
const cssFiles = [
|
||||
'.nuxt/dist/client/_nuxt/entry.DXd6OtrS.css',
|
||||
'.output/public/_nuxt/entry.DXd6OtrS.css',
|
||||
'assets/main.css'
|
||||
];
|
||||
|
||||
for (const file of cssFiles) {
|
||||
const fullPath = path.resolve(__dirname, file);
|
||||
if (fs.existsSync(fullPath)) {
|
||||
console.log(`Reading CSS from: ${fullPath}`);
|
||||
return fs.readFileSync(fullPath, 'utf-8');
|
||||
}
|
||||
}
|
||||
|
||||
console.warn('No CSS file found, using empty string');
|
||||
return '';
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
{
|
||||
name: 'inject-css-content',
|
||||
transform(code, id) {
|
||||
// Replace CSS import with actual content
|
||||
if (id.includes('vue-mount-app')) {
|
||||
const cssContent = getCssContent();
|
||||
const replacement = `const tailwindStyles = ${JSON.stringify(cssContent)};`;
|
||||
|
||||
// Replace the import statement
|
||||
code = code.replace(
|
||||
/import tailwindStyles from ['"]~\/assets\/main\.css\?inline['"];?/,
|
||||
replacement
|
||||
);
|
||||
|
||||
return code;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'~': resolve(__dirname, './'),
|
||||
'@': resolve(__dirname, './'),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: '.nuxt/standalone-apps',
|
||||
emptyOutDir: true,
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'components/standalone-mount.ts'),
|
||||
name: 'UnraidStandaloneApps',
|
||||
fileName: 'standalone-apps',
|
||||
formats: ['es'],
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [],
|
||||
output: {
|
||||
format: 'es',
|
||||
entryFileNames: 'standalone-apps.js',
|
||||
chunkFileNames: '[name]-[hash].js',
|
||||
assetFileNames: '[name]-[hash][extname]',
|
||||
inlineDynamicImports: false,
|
||||
},
|
||||
},
|
||||
cssCodeSplit: false,
|
||||
minify: 'terser',
|
||||
terserOptions: {
|
||||
mangle: {
|
||||
toplevel: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
define: {
|
||||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production'),
|
||||
},
|
||||
});
|
||||
26
web/vite.test.config.ts
Normal file
26
web/vite.test.config.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import { resolve } from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
root: '.',
|
||||
server: {
|
||||
port: 5173,
|
||||
open: '/test-standalone.html',
|
||||
cors: true,
|
||||
fs: {
|
||||
strict: false,
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'~': resolve(__dirname, './'),
|
||||
'@': resolve(__dirname, './'),
|
||||
'vue': 'vue/dist/vue.esm-bundler.js',
|
||||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ['vue', 'pinia', '@vue/apollo-composable'],
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user