Compare commits

..

19 Commits

Author SHA1 Message Date
Eli Bosley
8f0ec97eec Merge ee65e80435 into b7798b82f4 2025-08-19 14:30:25 -04:00
Eli Bosley
ee65e80435 refactor(api): remove CpuDataService from info and metrics modules
- Eliminated the `CpuDataService` from the `info.module.ts`, `metrics.module.ts`, and related integration tests, streamlining the CPU service implementation.
- Updated imports and provider lists to reflect the removal, ensuring the application remains functional without the redundant service.
2025-08-19 14:30:10 -04:00
Eli Bosley
8e258d30ed feat(api): add enum validation utility for improved type safety
- Introduced `isValidEnumValue` and `validateEnumValue` functions to validate enum values, enhancing type safety in the application.
- Updated `DisplayService` to utilize `validateEnumValue` for theme and unit properties, ensuring only valid enum values are assigned.
2025-08-19 14:20:30 -04:00
Eli Bosley
4d74a5e241 refactor(api): update memory metric field names for consistency
- Renamed `percentSwapUsed` to `percentSwapTotal` and `percentUsed` to `percentTotal` in the GraphQL schema to align with recent naming conventions.
- Adjusted export statements in the index file for consistency in quotation style.
2025-08-19 14:12:33 -04:00
Eli Bosley
57ef525d8e refactor(api): standardize CPU and memory metric field names
- Renamed CPU and memory metric fields for consistency, changing `load` to `percentTotal`, `loadUser` to `percentUser`, and similar adjustments for other fields across the GraphQL schema and resolvers.
- Updated integration tests to reflect the new naming conventions, ensuring accurate property checks for CPU and memory utilization.
- Enhanced the `CpuService` and `MemoryService` to return the updated metric names, improving clarity in the API response.
2025-08-19 14:11:59 -04:00
Eli Bosley
9df941317a refactor(api): update CPU and memory metrics naming conventions
- Renamed CPU and memory metric fields for consistency, changing `load` to `percentTotal`, `loadUser` to `percentUser`, and similar adjustments for other fields.
- Updated integration tests to reflect the new naming conventions, ensuring accurate property checks for CPU and memory utilization.
- Enhanced the `CpuService` and `MemoryService` to return the updated metric names, improving clarity in the API response.
2025-08-19 14:07:20 -04:00
Eli Bosley
25ff13b0bb fix(api): ensure proper cleanup in InfoResolver integration tests
- Added a null check for the `module` before calling `close()` in the `afterEach` hook to prevent potential errors during test teardown.
2025-08-19 13:36:38 -04:00
Eli Bosley
6930bb0500 feat(api): integrate ScheduleModule for task scheduling
- Added `ScheduleModule` to the main application module for managing scheduled tasks.
- Removed redundant `ScheduleModule` imports from `CronModule` and `ServicesModule` to streamline module dependencies.
2025-08-19 13:36:03 -04:00
Eli Bosley
b4a761c168 test(api): enhance integration tests for MetricsResolver with SubscriptionPollingService
- Integrated `ScheduleModule` to manage polling intervals effectively.
- Updated tests to utilize `SubscriptionPollingService` for CPU and memory polling, ensuring single execution during concurrent attempts.
- Improved error handling in polling tests to verify graceful error logging.
- Ensured proper cleanup of polling subscriptions and timers during module destruction.
2025-08-19 13:04:46 -04:00
Eli Bosley
ca691b71aa refactor(api): enhance metrics polling mechanism and simplify subscription handling
- Removed legacy polling methods from `MetricsResolver` and integrated polling logic into `SubscriptionPollingService` for better separation of concerns.
- Updated `SubscriptionTrackerService` to support polling configuration directly, allowing for cleaner topic registration.
- Adjusted unit tests to accommodate changes in the subscription handling and polling logic.
- Introduced `SubscriptionPollingService` to manage polling intervals and ensure efficient execution of polling tasks.
2025-08-19 12:50:17 -04:00
Eli Bosley
99da8bf309 fix(api): enhance type safety in pubsub subscription methods
- Updated `createSubscription` and `createTrackedSubscription` methods to include generic type parameters, improving type safety and ensuring correct async iterable handling.
2025-08-19 12:32:02 -04:00
Eli Bosley
0fadb6fbd9 feat: extract CPU and memory metrics from info resolver and move to metrics resolver (#1594)
- also enables subscription resolution for CPU and memory usage
2025-08-19 12:26:36 -04:00
Eli Bosley
573b04813d feat(api): expand GraphQL schema with new types and mutations for remote access and connection settings
- Introduced new types including `AccessUrl`, `Cloud`, `RemoteAccess`, and `Connect` to enhance remote access capabilities.
- Added enums for `URL_TYPE`, `WAN_ACCESS_TYPE`, and `WAN_FORWARD_TYPE` to standardize access configurations.
- Implemented new input types for connection settings and remote access setup, improving API usability.
- Updated the `Query` and `Mutation` types to include new fields for managing remote access and connection settings.
- Bumped API version to 4.13.1 in configuration.
2025-08-18 21:21:50 -04:00
Eli Bosley
f8cfe38d6f feat(api): introduce SubscriptionHelperService for improved subscription management
- Added `SubscriptionHelperService` to streamline the creation of tracked GraphQL subscriptions with automatic cleanup.
- Updated `InfoResolver` to utilize the new service for managing CPU utilization subscriptions, enhancing code clarity and maintainability.
- Introduced unit tests for `SubscriptionHelperService` and `SubscriptionTrackerService` to ensure robust functionality and reliability.
- Refactored `SubscriptionTrackerService` to include logging for subscription events, improving observability.
- Removed deprecated types and methods related to previous subscription handling, simplifying the codebase.
2025-08-18 21:21:50 -04:00
Eli Bosley
3afc4219ee refactor(api): streamline InfoResolver and SubscriptionTrackerService
- Removed unnecessary imports and parameters in InfoResolver for cleaner code.
- Updated CPU utilization method to directly use infoService for generating CPU load.
- Enhanced SubscriptionTrackerService to improve subscriber count management and added early return for idempotency in unsubscribe method.
2025-08-18 18:40:28 -04:00
Eli Bosley
476abd5f53 refactor(api): update CPU utilization handling in InfoResolver
- Changed CPU utilization topic references to use the new PUBSUB_CHANNEL enum for consistency.
- Updated InfoResolver to handle CPU polling timer as potentially undefined, improving type safety.
- Adjusted import statement for SubscriptionTrackerService to include the correct file extension.
2025-08-18 18:40:28 -04:00
Eli Bosley
a08726aad7 refactor(api): improve CPU load data caching in CpuDataService
- Updated `CpuDataService` to use nullish coalescing for initializing `cpuLoadData`, enhancing readability and efficiency.
- Ensured `cpuLoadData` can be undefined, allowing for more flexible handling of CPU load data retrieval.
2025-08-18 18:40:28 -04:00
Eli Bosley
ed98206769 refactor(api): reorganize info module imports and enhance CPU data handling
- Consolidated imports in the info resolver and service for better readability.
- Added `CpuDataService` to manage CPU load data more efficiently.
- Updated `InfoResolver` to include `InfoCpuResolver` for improved CPU data handling.
- Enhanced documentation for CPU load fields in the model to improve clarity.
- Streamlined the `SubscriptionTrackerService` for better topic management.
2025-08-18 18:40:28 -04:00
google-labs-jules[bot]
f7e1d8f259 feat(api): add cpu utilization query and subscription
Adds a new query and subscription to the info resolver for CPU to get CPU utilization on Unraid.

- Adds `cpuUtilization` query to get a one-time snapshot of CPU load.
- Adds `cpuUtilizationSubscription` to get real-time updates on CPU load.
- Adds a `utilization` field to the `InfoCpu` type.
- Creates a generic `SubscriptionTrackerService` to manage polling for subscriptions, ensuring that polling only occurs when there are active subscribers.
- Creates a request-scoped `CpuDataService` to cache CPU load data within a single GraphQL request to improve performance.
- Updates tests to cover the new functionality.
- Adds detailed documentation to the `CpuLoad` object type.
2025-08-18 18:40:28 -04:00
30 changed files with 329 additions and 506 deletions

View File

@@ -1 +1 @@
{".":"4.15.0"}
{".":"4.13.1"}

View File

@@ -1,28 +1,5 @@
# Changelog
## [4.15.0](https://github.com/unraid/api/compare/v4.14.0...v4.15.0) (2025-08-20)
### Features
* **api:** restructure versioning information in GraphQL schema ([#1600](https://github.com/unraid/api/issues/1600)) ([d0c6602](https://github.com/unraid/api/commit/d0c66020e1d1d5b6fcbc4ee8979bba4b3d34c7ad))
## [4.14.0](https://github.com/unraid/api/compare/v4.13.1...v4.14.0) (2025-08-19)
### Features
* **api:** add cpu utilization query and subscription ([#1590](https://github.com/unraid/api/issues/1590)) ([2b4c2a2](https://github.com/unraid/api/commit/2b4c2a264bb2769f88c3000d16447889cae57e98))
* enhance OIDC claim evaluation with array handling ([#1596](https://github.com/unraid/api/issues/1596)) ([b7798b8](https://github.com/unraid/api/commit/b7798b82f44aae9a428261270fd9dbde35ff7751))
### Bug Fixes
* remove unraid-api sso users & always apply sso modification on < 7.2 ([#1595](https://github.com/unraid/api/issues/1595)) ([4262830](https://github.com/unraid/api/commit/426283011afd41e3af7e48cfbb2a2d351c014bd1))
* update Docusaurus PR workflow to process and copy API docs ([3a10871](https://github.com/unraid/api/commit/3a10871918fe392a1974b69d16a135546166e058))
* update OIDC provider setup documentation for navigation clarity ([1a01696](https://github.com/unraid/api/commit/1a01696dc7b947abf5f2f097de1b231d5593c2ff))
* update OIDC provider setup documentation for redirect URI and screenshots ([1bc5251](https://github.com/unraid/api/commit/1bc52513109436b3ce8237c3796af765e208f9fc))
## [4.13.1](https://github.com/unraid/api/compare/v4.13.0...v4.13.1) (2025-08-15)

View File

@@ -1,5 +1,5 @@
{
"version": "4.14.0",
"version": "4.13.1",
"extraOrigins": [],
"sandbox": true,
"ssoSubIds": [],

View File

@@ -7,34 +7,32 @@ sidebar_position: 1
# Welcome to Unraid API
:::tip[What's New]
Starting with Unraid OS v7.2, the API comes built into the operating system - no plugin installation required!
Native integration in Unraid v7.2+ brings the API directly into the OS - no plugin needed!
:::
The Unraid API provides a GraphQL interface for programmatic interaction with your Unraid server. It enables automation, monitoring, and integration capabilities.
## 📦 Availability
### ✨ Native Integration (Unraid OS v7.2+)
### ✨ Native Integration (Unraid v7.2-beta.1+)
Starting with Unraid OS v7.2, the API is integrated directly into the operating system:
Starting with Unraid v7.2-beta.1, the API is integrated directly into the Unraid operating system:
- No plugin installation required
- Automatically available on system startup
- Deep system integration
- Access through **Settings****Management Access****API**
### 🔌 Plugin Installation (Pre-7.2 and Advanced Users)
### 🔌 Plugin Installation (Earlier Versions)
For Unraid versions prior to v7.2 or to access newer API features:
For Unraid versions prior to v7.2:
1. Install the Unraid Connect Plugin from Community Apps
1. Install Unraid Connect Plugin from Apps
2. [Configure the plugin](./how-to-use-the-api.md#enabling-the-graphql-sandbox)
3. Access API functionality through the [GraphQL Sandbox](./how-to-use-the-api.md)
:::info Important Notes
- The Unraid Connect plugin provides the API for pre-7.2 versions
- You do NOT need to sign in to Unraid Connect to use the API locally
- Installing the plugin on 7.2+ gives you access to newer API features before they're included in OS releases
:::tip Pre-release Versions
You can install the Unraid Connect plugin on any version to access pre-release versions of the API and get early access to new features before they're included in Unraid OS releases.
:::
## 📚 Documentation Sections
@@ -71,22 +69,20 @@ The API provides:
## 🚀 Get Started
<tabs>
<tabItem value="v72" label="Unraid OS v7.2+" default>
<tabItem value="v72" label="Unraid v7.2+" default>
1. The API is already installed and running
2. Access settings at **Settings****Management Access****API**
3. Enable the GraphQL Sandbox for development
4. Create your first API key
5. Start making GraphQL queries!
1. Access the API settings at **Settings****Management Access****API**
2. Enable the GraphQL Sandbox for development
3. Create your first API key
4. Start making GraphQL queries!
</tabItem>
<tabItem value="older" label="Pre-7.2 Versions">
<tabItem value="older" label="Earlier Versions">
1. Install the Unraid Connect plugin from Community Apps
2. No Unraid Connect login required for local API access
3. Configure the plugin settings
4. Enable the GraphQL Sandbox
5. Start exploring the API!
1. Install the Unraid Connect plugin from Apps
2. Configure the plugin settings
3. Enable the GraphQL Sandbox
4. Start exploring the API!
</tabItem>
</tabs>

View File

@@ -1501,51 +1501,98 @@ type InfoBaseboard implements Node {
memSlots: Float
}
type CoreVersions {
"""Unraid version"""
unraid: String
"""Unraid API version"""
api: String
type InfoVersions implements Node {
id: PrefixedID!
"""Kernel version"""
kernel: String
}
type PackageVersions {
"""OpenSSL version"""
openssl: String
"""System OpenSSL version"""
systemOpenssl: String
"""Node.js version"""
node: String
"""V8 engine version"""
v8: String
"""npm version"""
npm: String
"""Yarn version"""
yarn: String
"""pm2 version"""
pm2: String
"""Gulp version"""
gulp: String
"""Grunt version"""
grunt: String
"""Git version"""
git: String
"""tsc version"""
tsc: String
"""MySQL version"""
mysql: String
"""Redis version"""
redis: String
"""MongoDB version"""
mongodb: String
"""Apache version"""
apache: String
"""nginx version"""
nginx: String
"""PHP version"""
php: String
"""Postfix version"""
postfix: String
"""PostgreSQL version"""
postgresql: String
"""Perl version"""
perl: String
"""Python version"""
python: String
"""Python3 version"""
python3: String
"""pip version"""
pip: String
"""pip3 version"""
pip3: String
"""Java version"""
java: String
"""gcc version"""
gcc: String
"""VirtualBox version"""
virtualbox: String
"""Docker version"""
docker: String
}
type InfoVersions implements Node {
id: PrefixedID!
"""Core system versions"""
core: CoreVersions!
"""Software package versions"""
packages: PackageVersions!
"""Unraid version"""
unraid: String
}
type Info implements Node {

View File

@@ -1,6 +1,6 @@
{
"name": "@unraid/api",
"version": "4.15.0",
"version": "4.13.1",
"main": "src/cli/index.ts",
"type": "module",
"corepack": {

View File

@@ -64,13 +64,9 @@ describe('ApiReportService', () => {
uuid: 'test-uuid',
},
versions: {
core: {
unraid: '6.12.0',
kernel: '5.19.17',
},
packages: {
openssl: '3.0.8',
},
unraid: '6.12.0',
kernel: '5.19.17',
openssl: '3.0.8',
},
},
config: {

View File

@@ -82,7 +82,7 @@ export class ApiReportService {
? {
id: systemData.info.system.uuid,
name: systemData.server?.name || 'Unknown',
version: systemData.info.versions.core.unraid || 'Unknown',
version: systemData.info.versions.unraid || 'Unknown',
machineId: 'REDACTED',
manufacturer: systemData.info.system.manufacturer,
model: systemData.info.system.model,

View File

@@ -20,7 +20,7 @@ type Documents = {
"\n mutation UpdateSandboxSettings($input: JSON!) {\n updateSettings(input: $input) {\n restartRequired\n values\n }\n }\n": typeof types.UpdateSandboxSettingsDocument,
"\n query GetPlugins {\n plugins {\n name\n version\n hasApiModule\n hasCliModule\n }\n }\n": typeof types.GetPluginsDocument,
"\n query GetSSOUsers {\n settings {\n api {\n ssoSubIds\n }\n }\n }\n": typeof types.GetSsoUsersDocument,
"\n query SystemReport {\n info {\n id\n machineId\n system {\n manufacturer\n model\n version\n sku\n serial\n uuid\n }\n versions {\n core {\n unraid\n kernel\n }\n packages {\n openssl\n }\n }\n }\n config {\n id\n valid\n error\n }\n server {\n id\n name\n }\n }\n": typeof types.SystemReportDocument,
"\n query SystemReport {\n info {\n id\n machineId\n system {\n manufacturer\n model\n version\n sku\n serial\n uuid\n }\n versions {\n unraid\n kernel\n openssl\n }\n }\n config {\n id\n valid\n error\n }\n server {\n id\n name\n }\n }\n": typeof types.SystemReportDocument,
"\n query ConnectStatus {\n connect {\n id\n dynamicRemoteAccess {\n enabledType\n runningType\n error\n }\n }\n }\n": typeof types.ConnectStatusDocument,
"\n query Services {\n services {\n id\n name\n online\n uptime {\n timestamp\n }\n version\n }\n }\n": typeof types.ServicesDocument,
"\n query ValidateOidcSession($token: String!) {\n validateOidcSession(token: $token) {\n valid\n username\n }\n }\n": typeof types.ValidateOidcSessionDocument,
@@ -32,7 +32,7 @@ const documents: Documents = {
"\n mutation UpdateSandboxSettings($input: JSON!) {\n updateSettings(input: $input) {\n restartRequired\n values\n }\n }\n": types.UpdateSandboxSettingsDocument,
"\n query GetPlugins {\n plugins {\n name\n version\n hasApiModule\n hasCliModule\n }\n }\n": types.GetPluginsDocument,
"\n query GetSSOUsers {\n settings {\n api {\n ssoSubIds\n }\n }\n }\n": types.GetSsoUsersDocument,
"\n query SystemReport {\n info {\n id\n machineId\n system {\n manufacturer\n model\n version\n sku\n serial\n uuid\n }\n versions {\n core {\n unraid\n kernel\n }\n packages {\n openssl\n }\n }\n }\n config {\n id\n valid\n error\n }\n server {\n id\n name\n }\n }\n": types.SystemReportDocument,
"\n query SystemReport {\n info {\n id\n machineId\n system {\n manufacturer\n model\n version\n sku\n serial\n uuid\n }\n versions {\n unraid\n kernel\n openssl\n }\n }\n config {\n id\n valid\n error\n }\n server {\n id\n name\n }\n }\n": types.SystemReportDocument,
"\n query ConnectStatus {\n connect {\n id\n dynamicRemoteAccess {\n enabledType\n runningType\n error\n }\n }\n }\n": types.ConnectStatusDocument,
"\n query Services {\n services {\n id\n name\n online\n uptime {\n timestamp\n }\n version\n }\n }\n": types.ServicesDocument,
"\n query ValidateOidcSession($token: String!) {\n validateOidcSession(token: $token) {\n valid\n username\n }\n }\n": types.ValidateOidcSessionDocument,
@@ -79,7 +79,7 @@ export function gql(source: "\n query GetSSOUsers {\n settings {\n
/**
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function gql(source: "\n query SystemReport {\n info {\n id\n machineId\n system {\n manufacturer\n model\n version\n sku\n serial\n uuid\n }\n versions {\n core {\n unraid\n kernel\n }\n packages {\n openssl\n }\n }\n }\n config {\n id\n valid\n error\n }\n server {\n id\n name\n }\n }\n"): (typeof documents)["\n query SystemReport {\n info {\n id\n machineId\n system {\n manufacturer\n model\n version\n sku\n serial\n uuid\n }\n versions {\n core {\n unraid\n kernel\n }\n packages {\n openssl\n }\n }\n }\n config {\n id\n valid\n error\n }\n server {\n id\n name\n }\n }\n"];
export function gql(source: "\n query SystemReport {\n info {\n id\n machineId\n system {\n manufacturer\n model\n version\n sku\n serial\n uuid\n }\n versions {\n unraid\n kernel\n openssl\n }\n }\n config {\n id\n valid\n error\n }\n server {\n id\n name\n }\n }\n"): (typeof documents)["\n query SystemReport {\n info {\n id\n machineId\n system {\n manufacturer\n model\n version\n sku\n serial\n uuid\n }\n versions {\n unraid\n kernel\n openssl\n }\n }\n config {\n id\n valid\n error\n }\n server {\n id\n name\n }\n }\n"];
/**
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/

View File

@@ -520,16 +520,6 @@ export enum ContainerState {
RUNNING = 'RUNNING'
}
export type CoreVersions = {
__typename?: 'CoreVersions';
/** Unraid API version */
api?: Maybe<Scalars['String']['output']>;
/** Kernel version */
kernel?: Maybe<Scalars['String']['output']>;
/** Unraid version */
unraid?: Maybe<Scalars['String']['output']>;
};
/** CPU load for a single core */
export type CpuLoad = {
__typename?: 'CpuLoad';
@@ -1049,11 +1039,67 @@ export type InfoUsb = Node & {
export type InfoVersions = Node & {
__typename?: 'InfoVersions';
/** Core system versions */
core: CoreVersions;
/** Apache version */
apache?: Maybe<Scalars['String']['output']>;
/** Docker version */
docker?: Maybe<Scalars['String']['output']>;
/** gcc version */
gcc?: Maybe<Scalars['String']['output']>;
/** Git version */
git?: Maybe<Scalars['String']['output']>;
/** Grunt version */
grunt?: Maybe<Scalars['String']['output']>;
/** Gulp version */
gulp?: Maybe<Scalars['String']['output']>;
id: Scalars['PrefixedID']['output'];
/** Software package versions */
packages: PackageVersions;
/** Java version */
java?: Maybe<Scalars['String']['output']>;
/** Kernel version */
kernel?: Maybe<Scalars['String']['output']>;
/** MongoDB version */
mongodb?: Maybe<Scalars['String']['output']>;
/** MySQL version */
mysql?: Maybe<Scalars['String']['output']>;
/** nginx version */
nginx?: Maybe<Scalars['String']['output']>;
/** Node.js version */
node?: Maybe<Scalars['String']['output']>;
/** npm version */
npm?: Maybe<Scalars['String']['output']>;
/** OpenSSL version */
openssl?: Maybe<Scalars['String']['output']>;
/** Perl version */
perl?: Maybe<Scalars['String']['output']>;
/** PHP version */
php?: Maybe<Scalars['String']['output']>;
/** pip version */
pip?: Maybe<Scalars['String']['output']>;
/** pip3 version */
pip3?: Maybe<Scalars['String']['output']>;
/** pm2 version */
pm2?: Maybe<Scalars['String']['output']>;
/** Postfix version */
postfix?: Maybe<Scalars['String']['output']>;
/** PostgreSQL version */
postgresql?: Maybe<Scalars['String']['output']>;
/** Python version */
python?: Maybe<Scalars['String']['output']>;
/** Python3 version */
python3?: Maybe<Scalars['String']['output']>;
/** Redis version */
redis?: Maybe<Scalars['String']['output']>;
/** System OpenSSL version */
systemOpenssl?: Maybe<Scalars['String']['output']>;
/** tsc version */
tsc?: Maybe<Scalars['String']['output']>;
/** Unraid version */
unraid?: Maybe<Scalars['String']['output']>;
/** V8 engine version */
v8?: Maybe<Scalars['String']['output']>;
/** VirtualBox version */
virtualbox?: Maybe<Scalars['String']['output']>;
/** Yarn version */
yarn?: Maybe<Scalars['String']['output']>;
};
export type InitiateFlashBackupInput = {
@@ -1480,26 +1526,6 @@ export type Owner = {
username: Scalars['String']['output'];
};
export type PackageVersions = {
__typename?: 'PackageVersions';
/** Docker version */
docker?: Maybe<Scalars['String']['output']>;
/** Git version */
git?: Maybe<Scalars['String']['output']>;
/** nginx version */
nginx?: Maybe<Scalars['String']['output']>;
/** Node.js version */
node?: Maybe<Scalars['String']['output']>;
/** npm version */
npm?: Maybe<Scalars['String']['output']>;
/** OpenSSL version */
openssl?: Maybe<Scalars['String']['output']>;
/** PHP version */
php?: Maybe<Scalars['String']['output']>;
/** pm2 version */
pm2?: Maybe<Scalars['String']['output']>;
};
export type ParityCheck = {
__typename?: 'ParityCheck';
/** Whether corrections are being written to parity */
@@ -2527,7 +2553,7 @@ export type GetSsoUsersQuery = { __typename?: 'Query', settings: { __typename?:
export type SystemReportQueryVariables = Exact<{ [key: string]: never; }>;
export type SystemReportQuery = { __typename?: 'Query', info: { __typename?: 'Info', id: any, machineId?: string | null, system: { __typename?: 'InfoSystem', manufacturer?: string | null, model?: string | null, version?: string | null, sku?: string | null, serial?: string | null, uuid?: string | null }, versions: { __typename?: 'InfoVersions', core: { __typename?: 'CoreVersions', unraid?: string | null, kernel?: string | null }, packages: { __typename?: 'PackageVersions', openssl?: string | null } } }, config: { __typename?: 'Config', id: any, valid?: boolean | null, error?: string | null }, server?: { __typename?: 'Server', id: any, name: string } | null };
export type SystemReportQuery = { __typename?: 'Query', info: { __typename?: 'Info', id: any, machineId?: string | null, system: { __typename?: 'InfoSystem', manufacturer?: string | null, model?: string | null, version?: string | null, sku?: string | null, serial?: string | null, uuid?: string | null }, versions: { __typename?: 'InfoVersions', unraid?: string | null, kernel?: string | null, openssl?: string | null } }, config: { __typename?: 'Config', id: any, valid?: boolean | null, error?: string | null }, server?: { __typename?: 'Server', id: any, name: string } | null };
export type ConnectStatusQueryVariables = Exact<{ [key: string]: never; }>;
@@ -2553,7 +2579,7 @@ export const UpdateSsoUsersDocument = {"kind":"Document","definitions":[{"kind":
export const UpdateSandboxSettingsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateSandboxSettings"},"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<UpdateSandboxSettingsMutation, UpdateSandboxSettingsMutationVariables>;
export const GetPluginsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetPlugins"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"plugins"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"hasApiModule"}},{"kind":"Field","name":{"kind":"Name","value":"hasCliModule"}}]}}]}}]} as unknown as DocumentNode<GetPluginsQuery, GetPluginsQueryVariables>;
export const GetSsoUsersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSSOUsers"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"settings"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"api"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ssoSubIds"}}]}}]}}]}}]} as unknown as DocumentNode<GetSsoUsersQuery, GetSsoUsersQueryVariables>;
export const SystemReportDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SystemReport"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"info"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"machineId"}},{"kind":"Field","name":{"kind":"Name","value":"system"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"manufacturer"}},{"kind":"Field","name":{"kind":"Name","value":"model"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"sku"}},{"kind":"Field","name":{"kind":"Name","value":"serial"}},{"kind":"Field","name":{"kind":"Name","value":"uuid"}}]}},{"kind":"Field","name":{"kind":"Name","value":"versions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"core"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unraid"}},{"kind":"Field","name":{"kind":"Name","value":"kernel"}}]}},{"kind":"Field","name":{"kind":"Name","value":"packages"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"openssl"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"config"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"valid"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"server"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode<SystemReportQuery, SystemReportQueryVariables>;
export const SystemReportDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SystemReport"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"info"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"machineId"}},{"kind":"Field","name":{"kind":"Name","value":"system"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"manufacturer"}},{"kind":"Field","name":{"kind":"Name","value":"model"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"sku"}},{"kind":"Field","name":{"kind":"Name","value":"serial"}},{"kind":"Field","name":{"kind":"Name","value":"uuid"}}]}},{"kind":"Field","name":{"kind":"Name","value":"versions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unraid"}},{"kind":"Field","name":{"kind":"Name","value":"kernel"}},{"kind":"Field","name":{"kind":"Name","value":"openssl"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"config"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"valid"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"server"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode<SystemReportQuery, SystemReportQueryVariables>;
export const ConnectStatusDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ConnectStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connect"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dynamicRemoteAccess"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"enabledType"}},{"kind":"Field","name":{"kind":"Name","value":"runningType"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]}}]} as unknown as DocumentNode<ConnectStatusQuery, ConnectStatusQueryVariables>;
export const ServicesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Services"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"services"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"online"}},{"kind":"Field","name":{"kind":"Name","value":"uptime"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"timestamp"}}]}},{"kind":"Field","name":{"kind":"Name","value":"version"}}]}}]}}]} as unknown as DocumentNode<ServicesQuery, ServicesQueryVariables>;
export const ValidateOidcSessionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ValidateOidcSession"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"token"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"validateOidcSession"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"token"},"value":{"kind":"Variable","name":{"kind":"Name","value":"token"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"valid"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}}]}}]} as unknown as DocumentNode<ValidateOidcSessionQuery, ValidateOidcSessionQueryVariables>;

View File

@@ -14,13 +14,9 @@ export const SYSTEM_REPORT_QUERY = gql(`
uuid
}
versions {
core {
unraid
kernel
}
packages {
openssl
}
unraid
kernel
openssl
}
}
config {

View File

@@ -8,8 +8,6 @@ import { DisplayService } from '@app/unraid-api/graph/resolvers/info/display/dis
import { InfoResolver } from '@app/unraid-api/graph/resolvers/info/info.resolver.js';
import { MemoryService } from '@app/unraid-api/graph/resolvers/info/memory/memory.service.js';
import { OsService } from '@app/unraid-api/graph/resolvers/info/os/os.service.js';
import { CoreVersionsResolver } from '@app/unraid-api/graph/resolvers/info/versions/core-versions.resolver.js';
import { VersionsResolver } from '@app/unraid-api/graph/resolvers/info/versions/versions.resolver.js';
import { VersionsService } from '@app/unraid-api/graph/resolvers/info/versions/versions.service.js';
import { ServicesModule } from '@app/unraid-api/graph/services/services.module.js';
@@ -21,8 +19,6 @@ import { ServicesModule } from '@app/unraid-api/graph/services/services.module.j
// Sub-resolvers
DevicesResolver,
VersionsResolver,
CoreVersionsResolver,
// Services
CpuService,
@@ -32,6 +28,6 @@ import { ServicesModule } from '@app/unraid-api/graph/services/services.module.j
VersionsService,
DisplayService,
],
exports: [InfoResolver, DevicesResolver, VersionsResolver, CoreVersionsResolver, DisplayService],
exports: [InfoResolver, DevicesResolver, DisplayService],
})
export class InfoModule {}

View File

@@ -165,12 +165,16 @@ describe('InfoResolver Integration Tests', () => {
expect(typeof result.platform).toBe('string');
});
it('should return versions stub for field resolvers', () => {
const result = infoResolver.versions();
it.skipIf(process.env.CI)('should return versions data from service', async () => {
const result = await infoResolver.versions();
expect(result).toHaveProperty('id', 'info/versions');
// Versions now returns a stub object, with actual data resolved via field resolvers
expect(Object.keys(result)).toEqual(['id']);
expect(result).toHaveProperty('unraid');
expect(result).toHaveProperty('kernel');
expect(result).toHaveProperty('node');
expect(result).toHaveProperty('npm');
// Verify unraid version from mock
expect(result.unraid).toBe('6.12.0');
});
});

View File

@@ -94,7 +94,7 @@ export class InfoResolver {
}
@ResolveField(() => InfoVersions)
public versions(): Partial<InfoVersions> {
public async versions(): Promise<InfoVersions> {
return this.versionsService.generateVersions();
}
}

View File

@@ -1,14 +0,0 @@
import { ResolveField, Resolver } from '@nestjs/graphql';
import { versions } from 'systeminformation';
import { CoreVersions } from '@app/unraid-api/graph/resolvers/info/versions/versions.model.js';
@Resolver(() => CoreVersions)
export class CoreVersionsResolver {
@ResolveField(() => String, { nullable: true })
async kernel(): Promise<string | undefined> {
const softwareVersions = await versions();
return softwareVersions.kernel;
}
}

View File

@@ -1,21 +0,0 @@
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
let cachedVersion: string | undefined;
export function getApiVersion(): string {
if (cachedVersion) {
return cachedVersion;
}
try {
const packagePath = join(process.cwd(), 'package.json');
const packageJson = JSON.parse(readFileSync(packagePath, 'utf-8'));
const version = packageJson.version || 'unknown';
cachedVersion = version;
return version;
} catch (error) {
console.error('Failed to read API version from package.json:', error);
return 'unknown';
}
}

View File

@@ -2,50 +2,95 @@ import { Field, ObjectType } from '@nestjs/graphql';
import { Node } from '@unraid/shared/graphql.model.js';
@ObjectType()
export class CoreVersions {
@Field(() => String, { nullable: true, description: 'Unraid version' })
unraid?: string;
@Field(() => String, { nullable: true, description: 'Unraid API version' })
api?: string;
@ObjectType({ implements: () => Node })
export class InfoVersions extends Node {
@Field(() => String, { nullable: true, description: 'Kernel version' })
kernel?: string;
}
@ObjectType()
export class PackageVersions {
@Field(() => String, { nullable: true, description: 'OpenSSL version' })
openssl?: string;
@Field(() => String, { nullable: true, description: 'System OpenSSL version' })
systemOpenssl?: string;
@Field(() => String, { nullable: true, description: 'Node.js version' })
node?: string;
@Field(() => String, { nullable: true, description: 'V8 engine version' })
v8?: string;
@Field(() => String, { nullable: true, description: 'npm version' })
npm?: string;
@Field(() => String, { nullable: true, description: 'Yarn version' })
yarn?: string;
@Field(() => String, { nullable: true, description: 'pm2 version' })
pm2?: string;
@Field(() => String, { nullable: true, description: 'Gulp version' })
gulp?: string;
@Field(() => String, { nullable: true, description: 'Grunt version' })
grunt?: string;
@Field(() => String, { nullable: true, description: 'Git version' })
git?: string;
@Field(() => String, { nullable: true, description: 'tsc version' })
tsc?: string;
@Field(() => String, { nullable: true, description: 'MySQL version' })
mysql?: string;
@Field(() => String, { nullable: true, description: 'Redis version' })
redis?: string;
@Field(() => String, { nullable: true, description: 'MongoDB version' })
mongodb?: string;
@Field(() => String, { nullable: true, description: 'Apache version' })
apache?: string;
@Field(() => String, { nullable: true, description: 'nginx version' })
nginx?: string;
@Field(() => String, { nullable: true, description: 'PHP version' })
php?: string;
@Field(() => String, { nullable: true, description: 'Postfix version' })
postfix?: string;
@Field(() => String, { nullable: true, description: 'PostgreSQL version' })
postgresql?: string;
@Field(() => String, { nullable: true, description: 'Perl version' })
perl?: string;
@Field(() => String, { nullable: true, description: 'Python version' })
python?: string;
@Field(() => String, { nullable: true, description: 'Python3 version' })
python3?: string;
@Field(() => String, { nullable: true, description: 'pip version' })
pip?: string;
@Field(() => String, { nullable: true, description: 'pip3 version' })
pip3?: string;
@Field(() => String, { nullable: true, description: 'Java version' })
java?: string;
@Field(() => String, { nullable: true, description: 'gcc version' })
gcc?: string;
@Field(() => String, { nullable: true, description: 'VirtualBox version' })
virtualbox?: string;
@Field(() => String, { nullable: true, description: 'Docker version' })
docker?: string;
}
@ObjectType({ implements: () => Node })
export class InfoVersions extends Node {
@Field(() => CoreVersions, { description: 'Core system versions' })
core!: CoreVersions;
@Field(() => PackageVersions, { description: 'Software package versions' })
packages!: PackageVersions;
@Field(() => String, { nullable: true, description: 'Unraid version' })
unraid?: string;
}

View File

@@ -1,43 +0,0 @@
import { ConfigService } from '@nestjs/config';
import { ResolveField, Resolver } from '@nestjs/graphql';
import { versions } from 'systeminformation';
import {
CoreVersions,
InfoVersions,
PackageVersions,
} from '@app/unraid-api/graph/resolvers/info/versions/versions.model.js';
@Resolver(() => InfoVersions)
export class VersionsResolver {
constructor(private readonly configService: ConfigService) {}
@ResolveField(() => CoreVersions)
core(): CoreVersions {
const unraid = this.configService.get<string>('store.emhttp.var.version') || 'unknown';
const api = this.configService.get<string>('api.version') || 'unknown';
return {
unraid,
api,
kernel: undefined, // Will be resolved separately if requested
};
}
@ResolveField(() => PackageVersions)
async packages(): Promise<PackageVersions> {
const softwareVersions = await versions();
return {
openssl: softwareVersions.openssl,
node: softwareVersions.node,
npm: softwareVersions.npm,
pm2: softwareVersions.pm2,
git: softwareVersions.git,
nginx: softwareVersions.nginx,
php: softwareVersions.php,
docker: softwareVersions.docker,
};
}
}

View File

@@ -1,12 +1,22 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { versions } from 'systeminformation';
import { InfoVersions } from '@app/unraid-api/graph/resolvers/info/versions/versions.model.js';
@Injectable()
export class VersionsService {
generateVersions(): Partial<InfoVersions> {
constructor(private readonly configService: ConfigService) {}
async generateVersions(): Promise<InfoVersions> {
const unraid = this.configService.get<string>('store.emhttp.var.version') || 'unknown';
const softwareVersions = await versions();
return {
id: 'info/versions',
unraid,
...softwareVersions,
};
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "unraid-monorepo",
"private": true,
"version": "4.15.0",
"version": "4.13.1",
"scripts": {
"build": "pnpm -r build",
"build:watch": " pnpm -r --parallel build:watch",

View File

@@ -1,6 +1,6 @@
{
"name": "@unraid/connect-plugin",
"version": "4.15.0",
"version": "4.13.1",
"private": true,
"dependencies": {
"commander": "14.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@unraid/ui",
"version": "4.15.0",
"version": "4.13.1",
"private": true,
"license": "GPL-2.0-or-later",
"type": "module",

View File

@@ -18,42 +18,13 @@ import HeaderOsVersion from '~/components/HeaderOsVersion.ce.vue';
import { useErrorsStore } from '~/store/errors';
import { useServerStore } from '~/store/server';
const testMockReleaseNotesUrl = 'http://mock.release.notes/v';
vi.mock('crypto-js/aes', () => ({ default: {} }));
vi.mock('@unraid/shared-callbacks', () => ({
useCallback: vi.fn(() => ({ send: vi.fn(), watcher: vi.fn() })),
}));
vi.mock('@unraid/ui', () => ({
Badge: {
name: 'Badge',
template: '<div><slot /></div>',
},
DropdownMenuRoot: {
name: 'DropdownMenuRoot',
template: '<div><slot /></div>',
},
DropdownMenuTrigger: {
name: 'DropdownMenuTrigger',
template: '<div><slot /></div>',
},
DropdownMenuContent: {
name: 'DropdownMenuContent',
template: '<div><slot /></div>',
},
DropdownMenuItem: {
name: 'DropdownMenuItem',
template: '<div><slot /></div>',
},
DropdownMenuLabel: {
name: 'DropdownMenuLabel',
template: '<div><slot /></div>',
},
DropdownMenuSeparator: {
name: 'DropdownMenuSeparator',
template: '<div />',
},
}));
vi.mock('@vue/apollo-composable', () => ({
useQuery: () => ({
result: { value: {} },
@@ -152,11 +123,13 @@ describe('HeaderOsVersion', () => {
vi.restoreAllMocks();
});
it('renders OS version button with correct version and no update status initially', () => {
const versionButton = wrapper.find('button[title*="Version Information"]');
it('renders OS version link with correct URL and no update status initially', () => {
const versionLink = wrapper.find('a[title*="release notes"]');
expect(versionButton.exists()).toBe(true);
expect(versionButton.text()).toContain('6.12.0');
expect(versionLink.exists()).toBe(true);
expect(versionLink.attributes('href')).toBe(`${testMockReleaseNotesUrl}6.12.0`);
expect(versionLink.text()).toContain('6.12.0');
expect(findUpdateStatusComponent()).toBeNull();
});

View File

@@ -1,19 +1,16 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { useQuery } from '@vue/apollo-composable';
import { BellAlertIcon, ExclamationTriangleIcon, InformationCircleIcon, DocumentTextIcon, ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid';
import { Badge, DropdownMenuRoot, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator } from '@unraid/ui';
import { WEBGUI_TOOLS_DOWNGRADE, WEBGUI_TOOLS_UPDATE } from '~/helpers/urls';
import { BellAlertIcon, ExclamationTriangleIcon, InformationCircleIcon } from '@heroicons/vue/24/solid';
import { Badge } from '@unraid/ui';
import { getReleaseNotesUrl, WEBGUI_TOOLS_DOWNGRADE, WEBGUI_TOOLS_UPDATE } from '~/helpers/urls';
import { useActivationCodeDataStore } from '~/components/Activation/store/activationCodeData';
import { useServerStore } from '~/store/server';
import { useUpdateOsStore } from '~/store/updateOs';
import { useUpdateOsActionsStore } from '~/store/updateOsActions';
import { INFO_VERSIONS_QUERY } from './UserProfile/versions.query';
import ReleaseNotesModal from '~/components/ReleaseNotesModal.vue';
const { t } = useI18n();
@@ -26,20 +23,6 @@ const { osVersion, rebootType, stateDataError } = storeToRefs(serverStore);
const { available, availableWithRenewal } = storeToRefs(updateOsStore);
const { rebootTypeText } = storeToRefs(updateOsActionsStore);
// Query for version information
const { result: versionsResult } = useQuery(INFO_VERSIONS_QUERY, null, {
fetchPolicy: 'cache-first',
});
// Use versions endpoint as primary source, fallback to store
const displayOsVersion = computed(() => versionsResult.value?.info?.versions?.core?.unraid || osVersion.value || null);
const apiVersion = computed(() => versionsResult.value?.info?.versions?.core?.api || null);
const showOsReleaseNotesModal = ref(false);
const openApiChangelog = () => {
window.open('https://github.com/unraid/api/releases', '_blank');
};
const unraidLogoHeaderLink = computed<{ href: string; title: string }>(() => {
if (partnerInfo.value?.partnerUrl) {
return {
@@ -111,56 +94,16 @@ const updateOsStatus = computed(() => {
</a>
<div class="flex flex-wrap justify-start gap-2">
<DropdownMenuRoot>
<DropdownMenuTrigger as-child>
<button
class="text-xs xs:text-sm flex flex-row items-center gap-x-1 font-semibold text-header-text-secondary hover:text-orange-dark focus:text-orange-dark hover:underline focus:underline leading-none"
:title="t('Version Information')"
>
<InformationCircleIcon class="fill-current w-3 h-3 xs:w-4 xs:h-4 shrink-0" />
{{ displayOsVersion }}
</button>
</DropdownMenuTrigger>
<DropdownMenuContent class="min-w-[200px]" align="start" :side-offset="4">
<DropdownMenuLabel>
{{ t('Version Information') }}
</DropdownMenuLabel>
<DropdownMenuItem disabled class="text-xs opacity-100">
<span class="flex justify-between w-full">
<span>{{ t('Unraid OS') }}</span>
<span class="font-semibold">{{ displayOsVersion || t('Unknown') }}</span>
</span>
</DropdownMenuItem>
<DropdownMenuItem disabled class="text-xs opacity-100">
<span class="flex justify-between w-full">
<span>{{ t('Unraid API') }}</span>
<span class="font-semibold">{{ apiVersion || t('Unknown') }}</span>
</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem @click="showOsReleaseNotesModal = true">
<span class="flex items-center gap-x-2">
<InformationCircleIcon class="w-4 h-4" />
{{ t('View OS Release Notes') }}
</span>
</DropdownMenuItem>
<DropdownMenuItem @click="openApiChangelog">
<span class="flex items-center justify-between w-full">
<span class="flex items-center gap-x-2">
<DocumentTextIcon class="w-4 h-4" />
{{ t('View API Changelog') }}
</span>
<ArrowTopRightOnSquareIcon class="w-3 h-3 opacity-60" />
</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenuRoot>
<a
class="text-xs xs:text-sm flex flex-row items-center gap-x-1 font-semibold text-header-text-secondary hover:text-orange-dark focus:text-orange-dark hover:underline focus:underline leading-none"
:title="t('View release notes')"
:href="getReleaseNotesUrl(osVersion).toString()"
target="_blank"
rel="noopener"
>
<InformationCircleIcon class="fill-current w-3 h-3 xs:w-4 xs:h-4 shrink-0" />
{{ osVersion }}
</a>
<component
:is="updateOsStatus.href ? 'a' : 'button'"
v-if="updateOsStatus"
@@ -182,14 +125,5 @@ const updateOsStatus = computed(() => {
</template>
</component>
</div>
<!-- OS Release Notes Modal -->
<ReleaseNotesModal
v-if="displayOsVersion"
:open="showOsReleaseNotesModal"
:version="displayOsVersion"
:t="t"
@close="showOsReleaseNotesModal = false"
/>
</div>
</template>

View File

@@ -1,93 +0,0 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import type { ComposerTranslation } from 'vue-i18n';
import { ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid';
import { BrandButton, BrandLoading } from '@unraid/ui';
import Modal from '~/components/Modal.vue';
import { getReleaseNotesUrl } from '~/helpers/urls';
export interface Props {
open: boolean;
version: string;
t: ComposerTranslation;
}
const props = defineProps<Props>();
const emit = defineEmits<{
close: [];
}>();
const iframeRef = ref<HTMLIFrameElement | null>(null);
const isLoading = ref(true);
const releaseNotesUrl = computed(() => {
return getReleaseNotesUrl(props.version).toString();
});
const handleIframeLoad = () => {
isLoading.value = false;
};
const handleClose = () => {
emit('close');
};
const openInNewTab = () => {
window.open(releaseNotesUrl.value, '_blank');
};
</script>
<template>
<Modal
:center-content="false"
max-width="max-w-[800px]"
:open="open"
:show-close-x="true"
:t="t"
:tall-content="true"
:title="`Unraid OS ${version} Release Notes`"
:disable-overlay-close="false"
@close="handleClose"
>
<template #main>
<div class="flex flex-col gap-4 min-w-[280px] sm:min-w-[400px]">
<!-- Loading state -->
<div
v-if="isLoading"
class="text-center flex flex-col justify-center w-full min-h-[250px] min-w-[280px] sm:min-w-[400px]"
>
<BrandLoading class="w-[150px] mx-auto mt-6" />
<p>Loading release notes</p>
</div>
<!-- iframe for release notes -->
<div class="w-[calc(100%+3rem)] h-[475px] -mx-6 -my-6">
<iframe
ref="iframeRef"
:src="releaseNotesUrl"
class="w-full h-full border-0 rounded-md"
sandbox="allow-scripts allow-same-origin"
title="Unraid Release Notes"
@load="handleIframeLoad"
/>
</div>
</div>
</template>
<template #footer>
<div class="flex justify-end">
<!-- View on docs button -->
<BrandButton
variant="underline"
:external="true"
:href="releaseNotesUrl"
:icon="ArrowTopRightOnSquareIcon"
aria-label="View on Docs"
@click="openInNewTab"
>
Open in New Tab
</BrandButton>
</div>
</template>
</Modal>
</template>

View File

@@ -1,20 +0,0 @@
import { graphql } from '~/composables/gql';
export const INFO_VERSIONS_QUERY = graphql(/* GraphQL */ `
query InfoVersions {
info {
id
os {
id
hostname
}
versions {
id
core {
unraid
api
}
}
}
}
`);

View File

@@ -39,7 +39,7 @@ export function useHaveSeenNotifications() {
*
* Writing this ref will persist to local storage and affect global state.
*/
haveSeenNotifications: useStorage<boolean>(HAVE_SEEN_NOTIFICATIONS_KEY, false),
haveSeenNotifications: useStorage<boolean>(HAVE_SEEN_NOTIFICATIONS_KEY, null),
};
}

View File

@@ -44,7 +44,6 @@ type Documents = {
"\n mutation DeleteRCloneRemote($input: DeleteRCloneRemoteInput!) {\n rclone {\n deleteRCloneRemote(input: $input)\n }\n }\n": typeof types.DeleteRCloneRemoteDocument,
"\n query GetRCloneConfigForm($formOptions: RCloneConfigFormInput) {\n rclone {\n configForm(formOptions: $formOptions) {\n id\n dataSchema\n uiSchema\n }\n }\n }\n": typeof types.GetRCloneConfigFormDocument,
"\n query ListRCloneRemotes {\n rclone {\n remotes {\n name\n type\n parameters\n config\n }\n }\n }\n": typeof types.ListRCloneRemotesDocument,
"\n query InfoVersions {\n info {\n id\n os {\n id\n hostname\n }\n versions {\n id\n core {\n unraid\n api\n }\n }\n }\n }\n": typeof types.InfoVersionsDocument,
"\n query OidcProviders {\n settings {\n sso {\n oidcProviders {\n id\n name\n clientId\n issuer\n authorizationEndpoint\n tokenEndpoint\n jwksUri\n scopes\n authorizationRules {\n claim\n operator\n value\n }\n authorizationRuleMode\n buttonText\n buttonIcon\n }\n }\n }\n }\n": typeof types.OidcProvidersDocument,
"\n query PublicOidcProviders {\n publicOidcProviders {\n id\n name\n buttonText\n buttonIcon\n buttonVariant\n buttonStyle\n }\n }\n": typeof types.PublicOidcProvidersDocument,
"\n query serverInfo {\n info {\n os {\n hostname\n }\n }\n vars {\n comment\n }\n }\n": typeof types.ServerInfoDocument,
@@ -87,7 +86,6 @@ const documents: Documents = {
"\n mutation DeleteRCloneRemote($input: DeleteRCloneRemoteInput!) {\n rclone {\n deleteRCloneRemote(input: $input)\n }\n }\n": types.DeleteRCloneRemoteDocument,
"\n query GetRCloneConfigForm($formOptions: RCloneConfigFormInput) {\n rclone {\n configForm(formOptions: $formOptions) {\n id\n dataSchema\n uiSchema\n }\n }\n }\n": types.GetRCloneConfigFormDocument,
"\n query ListRCloneRemotes {\n rclone {\n remotes {\n name\n type\n parameters\n config\n }\n }\n }\n": types.ListRCloneRemotesDocument,
"\n query InfoVersions {\n info {\n id\n os {\n id\n hostname\n }\n versions {\n id\n core {\n unraid\n api\n }\n }\n }\n }\n": types.InfoVersionsDocument,
"\n query OidcProviders {\n settings {\n sso {\n oidcProviders {\n id\n name\n clientId\n issuer\n authorizationEndpoint\n tokenEndpoint\n jwksUri\n scopes\n authorizationRules {\n claim\n operator\n value\n }\n authorizationRuleMode\n buttonText\n buttonIcon\n }\n }\n }\n }\n": types.OidcProvidersDocument,
"\n query PublicOidcProviders {\n publicOidcProviders {\n id\n name\n buttonText\n buttonIcon\n buttonVariant\n buttonStyle\n }\n }\n": types.PublicOidcProvidersDocument,
"\n query serverInfo {\n info {\n os {\n hostname\n }\n }\n vars {\n comment\n }\n }\n": types.ServerInfoDocument,
@@ -234,10 +232,6 @@ export function graphql(source: "\n query GetRCloneConfigForm($formOptions: RCl
* 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 ListRCloneRemotes {\n rclone {\n remotes {\n name\n type\n parameters\n config\n }\n }\n }\n"): (typeof documents)["\n query ListRCloneRemotes {\n rclone {\n remotes {\n name\n type\n parameters\n config\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 query InfoVersions {\n info {\n id\n os {\n id\n hostname\n }\n versions {\n id\n core {\n unraid\n api\n }\n }\n }\n }\n"): (typeof documents)["\n query InfoVersions {\n info {\n id\n os {\n id\n hostname\n }\n versions {\n id\n core {\n unraid\n api\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/

View File

@@ -520,16 +520,6 @@ export enum ContainerState {
RUNNING = 'RUNNING'
}
export type CoreVersions = {
__typename?: 'CoreVersions';
/** Unraid API version */
api?: Maybe<Scalars['String']['output']>;
/** Kernel version */
kernel?: Maybe<Scalars['String']['output']>;
/** Unraid version */
unraid?: Maybe<Scalars['String']['output']>;
};
/** CPU load for a single core */
export type CpuLoad = {
__typename?: 'CpuLoad';
@@ -1049,11 +1039,67 @@ export type InfoUsb = Node & {
export type InfoVersions = Node & {
__typename?: 'InfoVersions';
/** Core system versions */
core: CoreVersions;
/** Apache version */
apache?: Maybe<Scalars['String']['output']>;
/** Docker version */
docker?: Maybe<Scalars['String']['output']>;
/** gcc version */
gcc?: Maybe<Scalars['String']['output']>;
/** Git version */
git?: Maybe<Scalars['String']['output']>;
/** Grunt version */
grunt?: Maybe<Scalars['String']['output']>;
/** Gulp version */
gulp?: Maybe<Scalars['String']['output']>;
id: Scalars['PrefixedID']['output'];
/** Software package versions */
packages: PackageVersions;
/** Java version */
java?: Maybe<Scalars['String']['output']>;
/** Kernel version */
kernel?: Maybe<Scalars['String']['output']>;
/** MongoDB version */
mongodb?: Maybe<Scalars['String']['output']>;
/** MySQL version */
mysql?: Maybe<Scalars['String']['output']>;
/** nginx version */
nginx?: Maybe<Scalars['String']['output']>;
/** Node.js version */
node?: Maybe<Scalars['String']['output']>;
/** npm version */
npm?: Maybe<Scalars['String']['output']>;
/** OpenSSL version */
openssl?: Maybe<Scalars['String']['output']>;
/** Perl version */
perl?: Maybe<Scalars['String']['output']>;
/** PHP version */
php?: Maybe<Scalars['String']['output']>;
/** pip version */
pip?: Maybe<Scalars['String']['output']>;
/** pip3 version */
pip3?: Maybe<Scalars['String']['output']>;
/** pm2 version */
pm2?: Maybe<Scalars['String']['output']>;
/** Postfix version */
postfix?: Maybe<Scalars['String']['output']>;
/** PostgreSQL version */
postgresql?: Maybe<Scalars['String']['output']>;
/** Python version */
python?: Maybe<Scalars['String']['output']>;
/** Python3 version */
python3?: Maybe<Scalars['String']['output']>;
/** Redis version */
redis?: Maybe<Scalars['String']['output']>;
/** System OpenSSL version */
systemOpenssl?: Maybe<Scalars['String']['output']>;
/** tsc version */
tsc?: Maybe<Scalars['String']['output']>;
/** Unraid version */
unraid?: Maybe<Scalars['String']['output']>;
/** V8 engine version */
v8?: Maybe<Scalars['String']['output']>;
/** VirtualBox version */
virtualbox?: Maybe<Scalars['String']['output']>;
/** Yarn version */
yarn?: Maybe<Scalars['String']['output']>;
};
export type InitiateFlashBackupInput = {
@@ -1480,26 +1526,6 @@ export type Owner = {
username: Scalars['String']['output'];
};
export type PackageVersions = {
__typename?: 'PackageVersions';
/** Docker version */
docker?: Maybe<Scalars['String']['output']>;
/** Git version */
git?: Maybe<Scalars['String']['output']>;
/** nginx version */
nginx?: Maybe<Scalars['String']['output']>;
/** Node.js version */
node?: Maybe<Scalars['String']['output']>;
/** npm version */
npm?: Maybe<Scalars['String']['output']>;
/** OpenSSL version */
openssl?: Maybe<Scalars['String']['output']>;
/** PHP version */
php?: Maybe<Scalars['String']['output']>;
/** pm2 version */
pm2?: Maybe<Scalars['String']['output']>;
};
export type ParityCheck = {
__typename?: 'ParityCheck';
/** Whether corrections are being written to parity */
@@ -2681,11 +2707,6 @@ export type ListRCloneRemotesQueryVariables = Exact<{ [key: string]: never; }>;
export type ListRCloneRemotesQuery = { __typename?: 'Query', rclone: { __typename?: 'RCloneBackupSettings', remotes: Array<{ __typename?: 'RCloneRemote', name: string, type: string, parameters: any, config: any }> } };
export type InfoVersionsQueryVariables = Exact<{ [key: string]: never; }>;
export type InfoVersionsQuery = { __typename?: 'Query', info: { __typename?: 'Info', id: string, os: { __typename?: 'InfoOs', id: string, hostname?: string | null }, versions: { __typename?: 'InfoVersions', id: string, core: { __typename?: 'CoreVersions', unraid?: string | null, api?: string | null } } } };
export type OidcProvidersQueryVariables = Exact<{ [key: string]: never; }>;
@@ -2769,7 +2790,6 @@ export const CreateRCloneRemoteDocument = {"kind":"Document","definitions":[{"ki
export const DeleteRCloneRemoteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteRCloneRemote"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DeleteRCloneRemoteInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"rclone"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteRCloneRemote"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]}}]} as unknown as DocumentNode<DeleteRCloneRemoteMutation, DeleteRCloneRemoteMutationVariables>;
export const GetRCloneConfigFormDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetRCloneConfigForm"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"formOptions"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"RCloneConfigFormInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"rclone"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configForm"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"formOptions"},"value":{"kind":"Variable","name":{"kind":"Name","value":"formOptions"}}}],"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"}}]}}]}}]}}]} as unknown as DocumentNode<GetRCloneConfigFormQuery, GetRCloneConfigFormQueryVariables>;
export const ListRCloneRemotesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ListRCloneRemotes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"rclone"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"remotes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"parameters"}},{"kind":"Field","name":{"kind":"Name","value":"config"}}]}}]}}]}}]} as unknown as DocumentNode<ListRCloneRemotesQuery, ListRCloneRemotesQueryVariables>;
export const InfoVersionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"InfoVersions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"info"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"os"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"hostname"}}]}},{"kind":"Field","name":{"kind":"Name","value":"versions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"core"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unraid"}},{"kind":"Field","name":{"kind":"Name","value":"api"}}]}}]}}]}}]}}]} as unknown as DocumentNode<InfoVersionsQuery, InfoVersionsQueryVariables>;
export const OidcProvidersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"OidcProviders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"settings"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sso"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"oidcProviders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"clientId"}},{"kind":"Field","name":{"kind":"Name","value":"issuer"}},{"kind":"Field","name":{"kind":"Name","value":"authorizationEndpoint"}},{"kind":"Field","name":{"kind":"Name","value":"tokenEndpoint"}},{"kind":"Field","name":{"kind":"Name","value":"jwksUri"}},{"kind":"Field","name":{"kind":"Name","value":"scopes"}},{"kind":"Field","name":{"kind":"Name","value":"authorizationRules"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"claim"}},{"kind":"Field","name":{"kind":"Name","value":"operator"}},{"kind":"Field","name":{"kind":"Name","value":"value"}}]}},{"kind":"Field","name":{"kind":"Name","value":"authorizationRuleMode"}},{"kind":"Field","name":{"kind":"Name","value":"buttonText"}},{"kind":"Field","name":{"kind":"Name","value":"buttonIcon"}}]}}]}}]}}]}}]} as unknown as DocumentNode<OidcProvidersQuery, OidcProvidersQueryVariables>;
export const PublicOidcProvidersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PublicOidcProviders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"publicOidcProviders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"buttonText"}},{"kind":"Field","name":{"kind":"Name","value":"buttonIcon"}},{"kind":"Field","name":{"kind":"Name","value":"buttonVariant"}},{"kind":"Field","name":{"kind":"Name","value":"buttonStyle"}}]}}]}}]} as unknown as DocumentNode<PublicOidcProvidersQuery, PublicOidcProvidersQueryVariables>;
export const ServerInfoDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"serverInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"info"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"os"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hostname"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"vars"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"comment"}}]}}]}}]} as unknown as DocumentNode<ServerInfoQuery, ServerInfoQueryVariables>;

View File

@@ -1,6 +1,6 @@
{
"name": "@unraid/web",
"version": "4.15.0",
"version": "4.13.1",
"private": true,
"license": "GPL-2.0-or-later",
"scripts": {