From d0c66020e1d1d5b6fcbc4ee8979bba4b3d34c7ad Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Wed, 20 Aug 2025 17:03:53 -0400 Subject: [PATCH] feat(api): restructure versioning information in GraphQL schema (#1600) --- api/dev/configs/api.json | 2 +- api/generated-schema.graphql | 81 +++----------- .../cli/__test__/api-report.service.test.ts | 10 +- api/src/unraid-api/cli/api-report.service.ts | 2 +- api/src/unraid-api/cli/generated/gql.ts | 6 +- api/src/unraid-api/cli/generated/graphql.ts | 98 +++++++---------- .../cli/queries/system-report.query.ts | 10 +- .../graph/resolvers/info/info.module.ts | 6 +- .../info/info.resolver.integration.spec.ts | 12 +-- .../graph/resolvers/info/info.resolver.ts | 2 +- .../info/versions/core-versions.resolver.ts | 14 +++ .../info/versions/get-api-version.ts | 21 ++++ .../resolvers/info/versions/versions.model.ts | 85 ++++----------- .../info/versions/versions.resolver.ts | 43 ++++++++ .../info/versions/versions.service.ts | 12 +-- .../components/HeaderOsVersion.test.ts | 43 ++++++-- web/components/HeaderOsVersion.ce.vue | 94 +++++++++++++--- web/components/ReleaseNotesModal.vue | 93 ++++++++++++++++ web/components/UserProfile/versions.query.ts | 20 ++++ web/composables/api/use-notifications.ts | 2 +- web/composables/gql/gql.ts | 6 ++ web/composables/gql/graphql.ts | 100 +++++++----------- 22 files changed, 456 insertions(+), 306 deletions(-) create mode 100644 api/src/unraid-api/graph/resolvers/info/versions/core-versions.resolver.ts create mode 100644 api/src/unraid-api/graph/resolvers/info/versions/get-api-version.ts create mode 100644 api/src/unraid-api/graph/resolvers/info/versions/versions.resolver.ts create mode 100644 web/components/ReleaseNotesModal.vue create mode 100644 web/components/UserProfile/versions.query.ts diff --git a/api/dev/configs/api.json b/api/dev/configs/api.json index df4db6b51..de99ec62a 100644 --- a/api/dev/configs/api.json +++ b/api/dev/configs/api.json @@ -1,5 +1,5 @@ { - "version": "4.13.1", + "version": "4.14.0", "extraOrigins": [], "sandbox": true, "ssoSubIds": [], diff --git a/api/generated-schema.graphql b/api/generated-schema.graphql index 25631cb05..b8f15b3b1 100644 --- a/api/generated-schema.graphql +++ b/api/generated-schema.graphql @@ -1501,98 +1501,51 @@ type InfoBaseboard implements Node { memSlots: Float } -type InfoVersions implements Node { - id: PrefixedID! +type CoreVersions { + """Unraid version""" + unraid: String + + """Unraid API version""" + api: String """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 +} - """Unraid version""" - unraid: String +type InfoVersions implements Node { + id: PrefixedID! + + """Core system versions""" + core: CoreVersions! + + """Software package versions""" + packages: PackageVersions! } type Info implements Node { diff --git a/api/src/unraid-api/cli/__test__/api-report.service.test.ts b/api/src/unraid-api/cli/__test__/api-report.service.test.ts index e518e2356..36f1f1473 100644 --- a/api/src/unraid-api/cli/__test__/api-report.service.test.ts +++ b/api/src/unraid-api/cli/__test__/api-report.service.test.ts @@ -64,9 +64,13 @@ describe('ApiReportService', () => { uuid: 'test-uuid', }, versions: { - unraid: '6.12.0', - kernel: '5.19.17', - openssl: '3.0.8', + core: { + unraid: '6.12.0', + kernel: '5.19.17', + }, + packages: { + openssl: '3.0.8', + }, }, }, config: { diff --git a/api/src/unraid-api/cli/api-report.service.ts b/api/src/unraid-api/cli/api-report.service.ts index 6264a1eb0..6b2ecac41 100644 --- a/api/src/unraid-api/cli/api-report.service.ts +++ b/api/src/unraid-api/cli/api-report.service.ts @@ -82,7 +82,7 @@ export class ApiReportService { ? { id: systemData.info.system.uuid, name: systemData.server?.name || 'Unknown', - version: systemData.info.versions.unraid || 'Unknown', + version: systemData.info.versions.core.unraid || 'Unknown', machineId: 'REDACTED', manufacturer: systemData.info.system.manufacturer, model: systemData.info.system.model, diff --git a/api/src/unraid-api/cli/generated/gql.ts b/api/src/unraid-api/cli/generated/gql.ts index 1f89d183f..7a8613665 100644 --- a/api/src/unraid-api/cli/generated/gql.ts +++ b/api/src/unraid-api/cli/generated/gql.ts @@ -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 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 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 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 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 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 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 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"]; +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"]; /** * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/api/src/unraid-api/cli/generated/graphql.ts b/api/src/unraid-api/cli/generated/graphql.ts index d0b08eeb9..72e473bf6 100644 --- a/api/src/unraid-api/cli/generated/graphql.ts +++ b/api/src/unraid-api/cli/generated/graphql.ts @@ -520,6 +520,16 @@ export enum ContainerState { RUNNING = 'RUNNING' } +export type CoreVersions = { + __typename?: 'CoreVersions'; + /** Unraid API version */ + api?: Maybe; + /** Kernel version */ + kernel?: Maybe; + /** Unraid version */ + unraid?: Maybe; +}; + /** CPU load for a single core */ export type CpuLoad = { __typename?: 'CpuLoad'; @@ -1039,67 +1049,11 @@ export type InfoUsb = Node & { export type InfoVersions = Node & { __typename?: 'InfoVersions'; - /** Apache version */ - apache?: Maybe; - /** Docker version */ - docker?: Maybe; - /** gcc version */ - gcc?: Maybe; - /** Git version */ - git?: Maybe; - /** Grunt version */ - grunt?: Maybe; - /** Gulp version */ - gulp?: Maybe; + /** Core system versions */ + core: CoreVersions; id: Scalars['PrefixedID']['output']; - /** Java version */ - java?: Maybe; - /** Kernel version */ - kernel?: Maybe; - /** MongoDB version */ - mongodb?: Maybe; - /** MySQL version */ - mysql?: Maybe; - /** nginx version */ - nginx?: Maybe; - /** Node.js version */ - node?: Maybe; - /** npm version */ - npm?: Maybe; - /** OpenSSL version */ - openssl?: Maybe; - /** Perl version */ - perl?: Maybe; - /** PHP version */ - php?: Maybe; - /** pip version */ - pip?: Maybe; - /** pip3 version */ - pip3?: Maybe; - /** pm2 version */ - pm2?: Maybe; - /** Postfix version */ - postfix?: Maybe; - /** PostgreSQL version */ - postgresql?: Maybe; - /** Python version */ - python?: Maybe; - /** Python3 version */ - python3?: Maybe; - /** Redis version */ - redis?: Maybe; - /** System OpenSSL version */ - systemOpenssl?: Maybe; - /** tsc version */ - tsc?: Maybe; - /** Unraid version */ - unraid?: Maybe; - /** V8 engine version */ - v8?: Maybe; - /** VirtualBox version */ - virtualbox?: Maybe; - /** Yarn version */ - yarn?: Maybe; + /** Software package versions */ + packages: PackageVersions; }; export type InitiateFlashBackupInput = { @@ -1526,6 +1480,26 @@ export type Owner = { username: Scalars['String']['output']; }; +export type PackageVersions = { + __typename?: 'PackageVersions'; + /** Docker version */ + docker?: Maybe; + /** Git version */ + git?: Maybe; + /** nginx version */ + nginx?: Maybe; + /** Node.js version */ + node?: Maybe; + /** npm version */ + npm?: Maybe; + /** OpenSSL version */ + openssl?: Maybe; + /** PHP version */ + php?: Maybe; + /** pm2 version */ + pm2?: Maybe; +}; + export type ParityCheck = { __typename?: 'ParityCheck'; /** Whether corrections are being written to parity */ @@ -2553,7 +2527,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', 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 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 ConnectStatusQueryVariables = Exact<{ [key: string]: never; }>; @@ -2579,7 +2553,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; 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; 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; -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; +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; 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; 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; 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; \ No newline at end of file diff --git a/api/src/unraid-api/cli/queries/system-report.query.ts b/api/src/unraid-api/cli/queries/system-report.query.ts index adc4140f8..5f4313458 100644 --- a/api/src/unraid-api/cli/queries/system-report.query.ts +++ b/api/src/unraid-api/cli/queries/system-report.query.ts @@ -14,9 +14,13 @@ export const SYSTEM_REPORT_QUERY = gql(` uuid } versions { - unraid - kernel - openssl + core { + unraid + kernel + } + packages { + openssl + } } } config { diff --git a/api/src/unraid-api/graph/resolvers/info/info.module.ts b/api/src/unraid-api/graph/resolvers/info/info.module.ts index a28a472b5..c9684061f 100644 --- a/api/src/unraid-api/graph/resolvers/info/info.module.ts +++ b/api/src/unraid-api/graph/resolvers/info/info.module.ts @@ -8,6 +8,8 @@ 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'; @@ -19,6 +21,8 @@ import { ServicesModule } from '@app/unraid-api/graph/services/services.module.j // Sub-resolvers DevicesResolver, + VersionsResolver, + CoreVersionsResolver, // Services CpuService, @@ -28,6 +32,6 @@ import { ServicesModule } from '@app/unraid-api/graph/services/services.module.j VersionsService, DisplayService, ], - exports: [InfoResolver, DevicesResolver, DisplayService], + exports: [InfoResolver, DevicesResolver, VersionsResolver, CoreVersionsResolver, DisplayService], }) export class InfoModule {} diff --git a/api/src/unraid-api/graph/resolvers/info/info.resolver.integration.spec.ts b/api/src/unraid-api/graph/resolvers/info/info.resolver.integration.spec.ts index 2745cfae8..e8ae31a70 100644 --- a/api/src/unraid-api/graph/resolvers/info/info.resolver.integration.spec.ts +++ b/api/src/unraid-api/graph/resolvers/info/info.resolver.integration.spec.ts @@ -165,16 +165,12 @@ describe('InfoResolver Integration Tests', () => { expect(typeof result.platform).toBe('string'); }); - it.skipIf(process.env.CI)('should return versions data from service', async () => { - const result = await infoResolver.versions(); + it('should return versions stub for field resolvers', () => { + const result = infoResolver.versions(); expect(result).toHaveProperty('id', 'info/versions'); - 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'); + // Versions now returns a stub object, with actual data resolved via field resolvers + expect(Object.keys(result)).toEqual(['id']); }); }); diff --git a/api/src/unraid-api/graph/resolvers/info/info.resolver.ts b/api/src/unraid-api/graph/resolvers/info/info.resolver.ts index c02180008..b029a24c0 100644 --- a/api/src/unraid-api/graph/resolvers/info/info.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/info/info.resolver.ts @@ -94,7 +94,7 @@ export class InfoResolver { } @ResolveField(() => InfoVersions) - public async versions(): Promise { + public versions(): Partial { return this.versionsService.generateVersions(); } } diff --git a/api/src/unraid-api/graph/resolvers/info/versions/core-versions.resolver.ts b/api/src/unraid-api/graph/resolvers/info/versions/core-versions.resolver.ts new file mode 100644 index 000000000..6ee8b2479 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/info/versions/core-versions.resolver.ts @@ -0,0 +1,14 @@ +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 { + const softwareVersions = await versions(); + return softwareVersions.kernel; + } +} diff --git a/api/src/unraid-api/graph/resolvers/info/versions/get-api-version.ts b/api/src/unraid-api/graph/resolvers/info/versions/get-api-version.ts new file mode 100644 index 000000000..c1ac6305c --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/info/versions/get-api-version.ts @@ -0,0 +1,21 @@ +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'; + } +} diff --git a/api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts b/api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts index 37ad2003a..e2971e51b 100644 --- a/api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts +++ b/api/src/unraid-api/graph/resolvers/info/versions/versions.model.ts @@ -2,95 +2,50 @@ import { Field, ObjectType } from '@nestjs/graphql'; import { Node } from '@unraid/shared/graphql.model.js'; -@ObjectType({ implements: () => Node }) -export class InfoVersions extends Node { +@ObjectType() +export class CoreVersions { + @Field(() => String, { nullable: true, description: 'Unraid version' }) + unraid?: string; + + @Field(() => String, { nullable: true, description: 'Unraid API version' }) + api?: string; + @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; - - @Field(() => String, { nullable: true, description: 'Unraid version' }) - unraid?: 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; } diff --git a/api/src/unraid-api/graph/resolvers/info/versions/versions.resolver.ts b/api/src/unraid-api/graph/resolvers/info/versions/versions.resolver.ts new file mode 100644 index 000000000..77d2f2d4f --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/info/versions/versions.resolver.ts @@ -0,0 +1,43 @@ +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('store.emhttp.var.version') || 'unknown'; + const api = this.configService.get('api.version') || 'unknown'; + + return { + unraid, + api, + kernel: undefined, // Will be resolved separately if requested + }; + } + + @ResolveField(() => PackageVersions) + async packages(): Promise { + 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, + }; + } +} diff --git a/api/src/unraid-api/graph/resolvers/info/versions/versions.service.ts b/api/src/unraid-api/graph/resolvers/info/versions/versions.service.ts index 42c399c19..c1cdce42f 100644 --- a/api/src/unraid-api/graph/resolvers/info/versions/versions.service.ts +++ b/api/src/unraid-api/graph/resolvers/info/versions/versions.service.ts @@ -1,22 +1,12 @@ 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 { - constructor(private readonly configService: ConfigService) {} - - async generateVersions(): Promise { - const unraid = this.configService.get('store.emhttp.var.version') || 'unknown'; - const softwareVersions = await versions(); - + generateVersions(): Partial { return { id: 'info/versions', - unraid, - ...softwareVersions, }; } } diff --git a/web/__test__/components/HeaderOsVersion.test.ts b/web/__test__/components/HeaderOsVersion.test.ts index 65c9287ff..6dea911ec 100644 --- a/web/__test__/components/HeaderOsVersion.test.ts +++ b/web/__test__/components/HeaderOsVersion.test.ts @@ -18,13 +18,42 @@ 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: '
', + }, + DropdownMenuRoot: { + name: 'DropdownMenuRoot', + template: '
', + }, + DropdownMenuTrigger: { + name: 'DropdownMenuTrigger', + template: '
', + }, + DropdownMenuContent: { + name: 'DropdownMenuContent', + template: '
', + }, + DropdownMenuItem: { + name: 'DropdownMenuItem', + template: '
', + }, + DropdownMenuLabel: { + name: 'DropdownMenuLabel', + template: '
', + }, + DropdownMenuSeparator: { + name: 'DropdownMenuSeparator', + template: '
', + }, +})); + vi.mock('@vue/apollo-composable', () => ({ useQuery: () => ({ result: { value: {} }, @@ -123,13 +152,11 @@ describe('HeaderOsVersion', () => { vi.restoreAllMocks(); }); - it('renders OS version link with correct URL and no update status initially', () => { - const versionLink = wrapper.find('a[title*="release notes"]'); + it('renders OS version button with correct version and no update status initially', () => { + const versionButton = wrapper.find('button[title*="Version Information"]'); - expect(versionLink.exists()).toBe(true); - expect(versionLink.attributes('href')).toBe(`${testMockReleaseNotesUrl}6.12.0`); - - expect(versionLink.text()).toContain('6.12.0'); + expect(versionButton.exists()).toBe(true); + expect(versionButton.text()).toContain('6.12.0'); expect(findUpdateStatusComponent()).toBeNull(); }); diff --git a/web/components/HeaderOsVersion.ce.vue b/web/components/HeaderOsVersion.ce.vue index bb76d4a59..165ffd8e2 100644 --- a/web/components/HeaderOsVersion.ce.vue +++ b/web/components/HeaderOsVersion.ce.vue @@ -1,16 +1,19 @@ + +