Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/src/__test__/store/modules/paths.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ test('Returns paths', async () => {
"keyfile-base",
"machine-id",
"log-base",
"unraid-log-base",
"var-run",
"auth-sessions",
"auth-keys",
Expand Down
1 change: 1 addition & 0 deletions api/src/core/pubsub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export enum PUBSUB_CHANNEL {
SERVERS = 'SERVERS',
VMS = 'VMS',
REGISTRATION = 'REGISTRATION',
LOG_FILE = 'LOG_FILE',
}

export const pubsub = new PubSub({ eventEmitter });
Expand Down
21 changes: 20 additions & 1 deletion api/src/graphql/generated/api/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import * as Types from '@app/graphql/generated/api/types.js';

import { z } from 'zod'
import { AccessUrl, AccessUrlInput, AddPermissionInput, AddRoleForApiKeyInput, AddRoleForUserInput, AllowedOriginInput, ApiKey, ApiKeyResponse, ApiKeyWithSecret, ApiSettingsInput, ArrayType, ArrayCapacity, ArrayDisk, ArrayDiskFsColor, ArrayDiskStatus, ArrayDiskType, ArrayPendingState, ArrayState, Baseboard, Capacity, Case, Cloud, CloudResponse, Config, ConfigErrorState, Connect, ConnectSettings, ConnectSettingsValues, ConnectSignInInput, ConnectUserInfoInput, ContainerHostConfig, ContainerMount, ContainerPort, ContainerPortType, ContainerState, CreateApiKeyInput, Devices, Disk, DiskFsType, DiskInterfaceType, DiskPartition, DiskSmartStatus, Display, Docker, DockerContainer, DockerNetwork, DynamicRemoteAccessStatus, DynamicRemoteAccessType, EnableDynamicRemoteAccessInput, Flash, Gpu, Importance, Info, InfoApps, InfoCpu, InfoMemory, KeyFile, Me, MemoryFormFactor, MemoryLayout, MemoryType, MinigraphStatus, MinigraphqlResponse, Mount, Network, Node, Notification, NotificationCounts, NotificationData, NotificationFilter, NotificationOverview, NotificationType, Notifications, NotificationslistArgs, Os, Owner, ParityCheck, Partition, Pci, Permission, ProfileModel, Registration, RegistrationState, RelayResponse, RemoteAccess, RemoveRoleFromApiKeyInput, Resource, Role, Server, ServerStatus, Service, SetupRemoteAccessInput, Share, System, Temperature, Theme, URL_TYPE, UnassignedDevice, Uptime, Usb, User, UserAccount, Vars, Versions, VmDomain, VmState, Vms, WAN_ACCESS_TYPE, WAN_FORWARD_TYPE, Welcome, addUserInput, arrayDiskInput, deleteUserInput, mdState, registrationType, usersInput } from '@app/graphql/generated/api/types.js'
import { AccessUrl, AccessUrlInput, AddPermissionInput, AddRoleForApiKeyInput, AddRoleForUserInput, AllowedOriginInput, ApiKey, ApiKeyResponse, ApiKeyWithSecret, ApiSettingsInput, ArrayType, ArrayCapacity, ArrayDisk, ArrayDiskFsColor, ArrayDiskStatus, ArrayDiskType, ArrayPendingState, ArrayState, Baseboard, Capacity, Case, Cloud, CloudResponse, Config, ConfigErrorState, Connect, ConnectSettings, ConnectSettingsValues, ConnectSignInInput, ConnectUserInfoInput, ContainerHostConfig, ContainerMount, ContainerPort, ContainerPortType, ContainerState, CreateApiKeyInput, Devices, Disk, DiskFsType, DiskInterfaceType, DiskPartition, DiskSmartStatus, Display, Docker, DockerContainer, DockerNetwork, DynamicRemoteAccessStatus, DynamicRemoteAccessType, EnableDynamicRemoteAccessInput, Flash, Gpu, Importance, Info, InfoApps, InfoCpu, InfoMemory, KeyFile, LogFile, LogFileContent, Me, MemoryFormFactor, MemoryLayout, MemoryType, MinigraphStatus, MinigraphqlResponse, Mount, Network, Node, Notification, NotificationCounts, NotificationData, NotificationFilter, NotificationOverview, NotificationType, Notifications, NotificationslistArgs, Os, Owner, ParityCheck, Partition, Pci, Permission, ProfileModel, Registration, RegistrationState, RelayResponse, RemoteAccess, RemoveRoleFromApiKeyInput, Resource, Role, Server, ServerStatus, Service, SetupRemoteAccessInput, Share, System, Temperature, Theme, URL_TYPE, UnassignedDevice, Uptime, Usb, User, UserAccount, Vars, Versions, VmDomain, VmState, Vms, WAN_ACCESS_TYPE, WAN_FORWARD_TYPE, Welcome, addUserInput, arrayDiskInput, deleteUserInput, mdState, registrationType, usersInput } from '@app/graphql/generated/api/types.js'
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';

type Properties<T> = Required<{
Expand Down Expand Up @@ -596,6 +596,25 @@ export function KeyFileSchema(): z.ZodObject<Properties<KeyFile>> {
})
}

export function LogFileSchema(): z.ZodObject<Properties<LogFile>> {
return z.object({
__typename: z.literal('LogFile').optional(),
modifiedAt: z.string(),
name: z.string(),
path: z.string(),
size: z.number()
})
}

export function LogFileContentSchema(): z.ZodObject<Properties<LogFileContent>> {
return z.object({
__typename: z.literal('LogFileContent').optional(),
content: z.string(),
path: z.string(),
totalLines: z.number()
})
}

export function MeSchema(): z.ZodObject<Properties<Me>> {
return z.object({
__typename: z.literal('Me').optional(),
Expand Down
89 changes: 88 additions & 1 deletion api/src/graphql/generated/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,10 @@ export type ApiSettingsInput = {
extraOrigins?: InputMaybe<Array<Scalars['String']['input']>>;
/** The type of port forwarding to use for Remote Access. */
forwardType?: InputMaybe<WAN_FORWARD_TYPE>;
/** The port to use for Remote Access. */
/**
* The port to use for Remote Access. Not required for UPNP forwardType. Required for STATIC forwardType.
* Ignored if accessType is DISABLED or forwardType is UPNP.
*/
port?: InputMaybe<Scalars['Port']['input']>;
/**
* If true, the GraphQL sandbox will be enabled and available at /graphql.
Expand Down Expand Up @@ -330,10 +333,18 @@ export type ConnectSettings = Node & {
/** Intersection type of ApiSettings and RemoteAccess */
export type ConnectSettingsValues = {
__typename?: 'ConnectSettingsValues';
/** The type of WAN access used for Remote Access. */
accessType: WAN_ACCESS_TYPE;
/** A list of origins allowed to interact with the API. */
extraOrigins: Array<Scalars['String']['output']>;
/** The type of port forwarding used for Remote Access. */
forwardType?: Maybe<WAN_FORWARD_TYPE>;
/** The port used for Remote Access. */
port?: Maybe<Scalars['Port']['output']>;
/**
* If true, the GraphQL sandbox is enabled and available at /graphql.
* If false, the GraphQL sandbox is disabled and only the production API will be available.
*/
sandbox: Scalars['Boolean']['output'];
};

Expand Down Expand Up @@ -634,6 +645,30 @@ export type KeyFile = {
location?: Maybe<Scalars['String']['output']>;
};

/** Represents a log file in the system */
export type LogFile = {
__typename?: 'LogFile';
/** Last modified timestamp */
modifiedAt: Scalars['DateTime']['output'];
/** Name of the log file */
name: Scalars['String']['output'];
/** Full path to the log file */
path: Scalars['String']['output'];
/** Size of the log file in bytes */
size: Scalars['Int']['output'];
};

/** Content of a log file */
export type LogFileContent = {
__typename?: 'LogFileContent';
/** Content of the log file */
content: Scalars['String']['output'];
/** Path to the log file */
path: Scalars['String']['output'];
/** Total number of lines in the file */
totalLines: Scalars['Int']['output'];
};

/** The current user */
export type Me = UserAccount & {
__typename?: 'Me';
Expand Down Expand Up @@ -744,6 +779,10 @@ export type Mutation = {
unmountArrayDisk?: Maybe<Disk>;
/** Marks a notification as unread. */
unreadNotification: Notification;
/**
* Update the API settings.
* Some setting combinations may be required or disallowed. Please refer to each setting for more information.
*/
updateApiSettings: ConnectSettingsValues;
};

Expand Down Expand Up @@ -1113,6 +1152,14 @@ export type Query = {
extraAllowedOrigins: Array<Scalars['String']['output']>;
flash?: Maybe<Flash>;
info?: Maybe<Info>;
/**
* Get the content of a specific log file
* @param path Path to the log file
* @param lines Number of lines to read from the end of the file (default: 100)
*/
logFile: LogFileContent;
/** List all available log files */
logFiles: Array<LogFile>;
/** Current user account */
me?: Maybe<Me>;
network?: Maybe<Network>;
Expand Down Expand Up @@ -1163,6 +1210,12 @@ export type QuerydockerNetworksArgs = {
};


export type QuerylogFileArgs = {
lines?: InputMaybe<Scalars['Int']['input']>;
path: Scalars['String']['input'];
};


export type QueryuserArgs = {
id: Scalars['ID']['input'];
};
Expand Down Expand Up @@ -1353,6 +1406,11 @@ export type Subscription = {
dockerNetworks: Array<Maybe<DockerNetwork>>;
flash: Flash;
info: Info;
/**
* Subscribe to changes in a log file
* @param path Path to the log file
*/
logFile: LogFileContent;
me?: Maybe<Me>;
notificationAdded: Notification;
notificationsOverview: NotificationOverview;
Expand Down Expand Up @@ -1383,6 +1441,11 @@ export type SubscriptiondockerNetworkArgs = {
};


export type SubscriptionlogFileArgs = {
path: Scalars['String']['input'];
};


export type SubscriptionserviceArgs = {
name: Scalars['String']['input'];
};
Expand Down Expand Up @@ -1927,6 +1990,8 @@ export type ResolversTypes = ResolversObject<{
Int: ResolverTypeWrapper<Scalars['Int']['output']>;
JSON: ResolverTypeWrapper<Scalars['JSON']['output']>;
KeyFile: ResolverTypeWrapper<KeyFile>;
LogFile: ResolverTypeWrapper<LogFile>;
LogFileContent: ResolverTypeWrapper<LogFileContent>;
Long: ResolverTypeWrapper<Scalars['Long']['output']>;
Me: ResolverTypeWrapper<Me>;
MemoryFormFactor: MemoryFormFactor;
Expand Down Expand Up @@ -2047,6 +2112,8 @@ export type ResolversParentTypes = ResolversObject<{
Int: Scalars['Int']['output'];
JSON: Scalars['JSON']['output'];
KeyFile: KeyFile;
LogFile: LogFile;
LogFileContent: LogFileContent;
Long: Scalars['Long']['output'];
Me: Me;
MemoryLayout: MemoryLayout;
Expand Down Expand Up @@ -2481,6 +2548,21 @@ export type KeyFileResolvers<ContextType = Context, ParentType extends Resolvers
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;

export type LogFileResolvers<ContextType = Context, ParentType extends ResolversParentTypes['LogFile'] = ResolversParentTypes['LogFile']> = ResolversObject<{
modifiedAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
path?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
size?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;

export type LogFileContentResolvers<ContextType = Context, ParentType extends ResolversParentTypes['LogFileContent'] = ResolversParentTypes['LogFileContent']> = ResolversObject<{
content?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
path?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
totalLines?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;

export interface LongScalarConfig extends GraphQLScalarTypeConfig<ResolversTypes['Long'], any> {
name: 'Long';
}
Expand Down Expand Up @@ -2763,6 +2845,8 @@ export type QueryResolvers<ContextType = Context, ParentType extends ResolversPa
extraAllowedOrigins?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType>;
flash?: Resolver<Maybe<ResolversTypes['Flash']>, ParentType, ContextType>;
info?: Resolver<Maybe<ResolversTypes['Info']>, ParentType, ContextType>;
logFile?: Resolver<ResolversTypes['LogFileContent'], ParentType, ContextType, RequireFields<QuerylogFileArgs, 'path'>>;
logFiles?: Resolver<Array<ResolversTypes['LogFile']>, ParentType, ContextType>;
me?: Resolver<Maybe<ResolversTypes['Me']>, ParentType, ContextType>;
network?: Resolver<Maybe<ResolversTypes['Network']>, ParentType, ContextType>;
notifications?: Resolver<ResolversTypes['Notifications'], ParentType, ContextType>;
Expand Down Expand Up @@ -2857,6 +2941,7 @@ export type SubscriptionResolvers<ContextType = Context, ParentType extends Reso
dockerNetworks?: SubscriptionResolver<Array<Maybe<ResolversTypes['DockerNetwork']>>, "dockerNetworks", ParentType, ContextType>;
flash?: SubscriptionResolver<ResolversTypes['Flash'], "flash", ParentType, ContextType>;
info?: SubscriptionResolver<ResolversTypes['Info'], "info", ParentType, ContextType>;
logFile?: SubscriptionResolver<ResolversTypes['LogFileContent'], "logFile", ParentType, ContextType, RequireFields<SubscriptionlogFileArgs, 'path'>>;
me?: SubscriptionResolver<Maybe<ResolversTypes['Me']>, "me", ParentType, ContextType>;
notificationAdded?: SubscriptionResolver<ResolversTypes['Notification'], "notificationAdded", ParentType, ContextType>;
notificationsOverview?: SubscriptionResolver<ResolversTypes['NotificationOverview'], "notificationsOverview", ParentType, ContextType>;
Expand Down Expand Up @@ -3212,6 +3297,8 @@ export type Resolvers<ContextType = Context> = ResolversObject<{
InfoMemory?: InfoMemoryResolvers<ContextType>;
JSON?: GraphQLScalarType;
KeyFile?: KeyFileResolvers<ContextType>;
LogFile?: LogFileResolvers<ContextType>;
LogFileContent?: LogFileContentResolvers<ContextType>;
Long?: GraphQLScalarType;
Me?: MeResolvers<ContextType>;
MemoryLayout?: MemoryLayoutResolvers<ContextType>;
Expand Down
72 changes: 72 additions & 0 deletions api/src/graphql/schema/types/logs/logs.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
type Query {
"""
List all available log files
"""
logFiles: [LogFile!]!

"""
Get the content of a specific log file
@param path Path to the log file
@param lines Number of lines to read from the end of the file (default: 100)
@param startLine Optional starting line number (1-indexed)
"""
logFile(path: String!, lines: Int, startLine: Int): LogFileContent!
}

type Subscription {
"""
Subscribe to changes in a log file
@param path Path to the log file
"""
logFile(path: String!): LogFileContent!
}

"""
Represents a log file in the system
"""
type LogFile {
"""
Name of the log file
"""
name: String!

"""
Full path to the log file
"""
path: String!

"""
Size of the log file in bytes
"""
size: Int!

"""
Last modified timestamp
"""
modifiedAt: DateTime!
}

"""
Content of a log file
"""
type LogFileContent {
"""
Path to the log file
"""
path: String!

"""
Content of the log file
"""
content: String!

"""
Total number of lines in the file
"""
totalLines: Int!

"""
Starting line number of the content (1-indexed)
"""
startLine: Int
}
1 change: 1 addition & 0 deletions api/src/store/modules/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const initialState = {
'keyfile-base': resolvePath(process.env.PATHS_KEYFILE_BASE ?? ('/boot/config' as const)),
'machine-id': resolvePath(process.env.PATHS_MACHINE_ID ?? ('/var/lib/dbus/machine-id' as const)),
'log-base': resolvePath('/var/log/unraid-api/' as const),
'unraid-log-base': resolvePath('/var/log/' as const),
'var-run': '/var/run' as const,
// contains sess_ files that correspond to authenticated user sessions
'auth-sessions': process.env.PATHS_AUTH_SESSIONS ?? '/var/lib/php',
Expand Down
10 changes: 10 additions & 0 deletions api/src/unraid-api/graph/resolvers/logs/logs.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';

import { LogsResolver } from '@app/unraid-api/graph/resolvers/logs/logs.resolver.js';
import { LogsService } from '@app/unraid-api/graph/resolvers/logs/logs.service.js';

@Module({
providers: [LogsResolver, LogsService],
exports: [LogsService],
})
export class LogsModule {}
30 changes: 30 additions & 0 deletions api/src/unraid-api/graph/resolvers/logs/logs.resolver.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Test, TestingModule } from '@nestjs/testing';

import { beforeEach, describe, expect, it } from 'vitest';

import { LogsResolver } from '@app/unraid-api/graph/resolvers/logs/logs.resolver.js';
import { LogsService } from '@app/unraid-api/graph/resolvers/logs/logs.service.js';

describe('LogsResolver', () => {
let resolver: LogsResolver;
let service: LogsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
LogsResolver,
{
provide: LogsService,
useValue: {
// Add mock implementations for service methods used by resolver
},
},
],
}).compile();
resolver = module.get<LogsResolver>(LogsResolver);
service = module.get<LogsService>(LogsService);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
// Add more tests for resolver methods
});
Loading
Loading