diff --git a/api/generated-schema.graphql b/api/generated-schema.graphql index a9f1e5841..ff52732ae 100644 --- a/api/generated-schema.graphql +++ b/api/generated-schema.graphql @@ -1353,34 +1353,12 @@ type RCloneDrive { options: JSON! } -type RCloneProviderOption { - name: String! - help: String! - provider: String! - default: JSON - value: JSON - shortOpt: String - hide: Boolean - required: Boolean - isPassword: Boolean - noPrefix: Boolean - advanced: Boolean - defaultStr: String - valueStr: String - type: String - examples: [RCloneProviderOptionExample!] -} - -type RCloneProviderOptionExample { - value: String! - help: String! - provider: String! -} - type RCloneBackupConfigForm { id: ID! - dataSchema(providerType: String, parameters: JSON): JSON! - uiSchema(providerType: String, parameters: JSON): JSON! + dataSchema: JSON! + uiSchema: JSON! + providerType: String + parameters: JSON } type RCloneBackupSettings { @@ -1392,7 +1370,7 @@ type RCloneBackupSettings { type RCloneRemote { name: String! type: String! - config: JSON! + parameters: JSON! } type Flash implements Node { diff --git a/api/src/unraid-api/graph/resolvers/rclone/jsonforms/rclone-jsonforms-config.ts b/api/src/unraid-api/graph/resolvers/rclone/jsonforms/rclone-jsonforms-config.ts index 7a6c4b0c6..592cc7390 100644 --- a/api/src/unraid-api/graph/resolvers/rclone/jsonforms/rclone-jsonforms-config.ts +++ b/api/src/unraid-api/graph/resolvers/rclone/jsonforms/rclone-jsonforms-config.ts @@ -1,38 +1,14 @@ -import { RuleEffect, type SchemaBasedCondition, type JsonSchema, JsonSchema7 } from '@jsonforms/core'; +import type { SchemaBasedCondition } from '@jsonforms/core'; +import { JsonSchema7, RuleEffect } from '@jsonforms/core'; + import type { DataSlice, SettingSlice, UIElement } from '@app/unraid-api/types/json-forms.js'; +import { RCloneProviderOptionResponse } from '@app/unraid-api/graph/resolvers/rclone/rclone.model.js'; import { mergeSettingSlices } from '@app/unraid-api/types/json-forms.js'; -import { config as rcloneConfig } from './config.js'; - -/** - * Type definitions for RClone config options - */ -export interface RCloneOptionDef { - Name: string; - Help: string; - Provider: string; - Default: any; - Value: any | null; - ShortOpt: string; - Hide: number; - Required: boolean; - IsPassword: boolean; - NoPrefix: boolean; - Advanced: boolean; - Type?: string; - Options?: string[]; -} - -export interface RCloneProviderDef { - Name: string; - Description: string; - Prefix: string; - Options: RCloneOptionDef[]; -} /** * Translates RClone config option to JsonSchema properties */ -function translateRCloneOptionToJsonSchema(option: RCloneOptionDef): JsonSchema7 { +function translateRCloneOptionToJsonSchema(option: RCloneProviderOptionResponse): JsonSchema7 { const schema: JsonSchema7 = { type: getJsonSchemaType(option.Type || 'string'), title: option.Name, @@ -41,64 +17,85 @@ function translateRCloneOptionToJsonSchema(option: RCloneOptionDef): JsonSchema7 // Add default value if available if (option.Default !== undefined && option.Default !== '') { - schema.default = option.Default; + // RClone uses 'off' for SizeSuffix/Duration defaults sometimes + if ((option.Type === 'SizeSuffix' || option.Type === 'Duration') && option.Default === 'off') { + schema.default = 'off'; + } else if (schema.type === 'number' && typeof option.Default === 'number') { + schema.default = option.Default; + } else if (schema.type === 'integer' && Number.isInteger(option.Default)) { + schema.default = option.Default; + } else if (schema.type === 'boolean' && typeof option.Default === 'boolean') { + schema.default = option.Default; + } else if (schema.type === 'string') { + // Ensure default is a string if the type is string + schema.default = String(option.Default); + } + // If type doesn't match, we might skip the default or log a warning } - // Add enum values if available - if (option.Options && option.Options.length > 0) { - schema.enum = option.Options; + // Add enum values if available (used for dropdowns) + if (option.Examples && option.Examples.length > 0) { + schema.enum = option.Examples.map((example) => example.Value); } - // Add validation constraints + // Add format hints + const format = getJsonFormElementForType( + option.Type, + option.Examples?.map((example) => example.Value), + option.IsPassword + ); + if (format && format !== schema.type) { + // Don't add format if it's just the type (e.g., 'number') + schema.format = format; + } + + // Add validation constraints and patterns if (option.Required) { + // Make '0' valid for required number/integer fields unless explicitly disallowed if (schema.type === 'string') { schema.minLength = 1; - } else if (schema.type === 'number') { - schema.minimum = 0; } + // Note: 'required' is usually handled at the object level in JSON Schema, + // but minLength/minimum provide basic non-empty checks. + } + + // Specific type-based validation + switch (option.Type?.toLowerCase()) { + case 'int': + // Handled by type: 'integer' in getJsonSchemaType + break; + case 'sizesuffix': + // Pattern allows 'off' or digits followed by optional size units (K, M, G, T, P) and optional iB/B + // Allows multiple concatenated values like 1G100M + schema.pattern = '^(off|(\\d+([KMGTPE]i?B?)?)+)$'; + schema.errorMessage = 'Invalid size format. Examples: "10G", "100M", "1.5GiB", "off".'; + break; + case 'duration': + // Pattern allows 'off' or digits (with optional decimal) followed by time units (ns, us, ms, s, m, h) + // Allows multiple concatenated values like 1h15m + schema.pattern = '^(off|(\\d+(\\.\\d+)?(ns|us|\\u00b5s|ms|s|m|h))+)$'; // \u00b5s is µs + schema.errorMessage = + 'Invalid duration format. Examples: "10s", "1.5m", "100ms", "1h15m", "off".'; + break; } return schema; } -/** - * Get available provider types from RClone config - */ -export function getAvailableProviderTypes(): string[] { - return rcloneConfig.map(provider => provider.Name); -} - -/** - * Get provider options for a specific provider - */ -export function getProviderOptions(providerName: string): Record { - const provider = rcloneConfig.find(p => p.Name === providerName); - if (!provider) return {}; - - return provider.Options.reduce((acc, option) => { - acc[option.Name] = option; - return acc; - }, {} as Record); -} - /** * Generates the UI schema for RClone remote configuration */ export function getRcloneConfigFormSchema( providerTypes: string[] = [], selectedProvider: string = '', - providerOptions: Record = {} + providerOptions: Record = {} ): SettingSlice { - // If provider types not provided, get from config - if (providerTypes.length === 0) { - providerTypes = getAvailableProviderTypes(); - } - // Combine all form slices for the complete schema + const options = providerOptions[selectedProvider]; const slices = [ getBasicConfigSlice(providerTypes), - getProviderConfigSlice(selectedProvider, providerOptions), - getAdvancedConfigSlice(selectedProvider, providerOptions), + getProviderConfigSlice(selectedProvider, options, false), // Standard options + getProviderConfigSlice(selectedProvider, options, true), // Advanced options ]; return mergeSettingSlices(slices); @@ -116,6 +113,7 @@ function getBasicConfigSlice(providerTypes: string[]): SettingSlice { label: 'Name of this remote (For your reference)', options: { placeholder: 'Enter a name', + format: 'string', }, }, { @@ -123,7 +121,6 @@ function getBasicConfigSlice(providerTypes: string[]): SettingSlice { scope: '#/properties/type', label: 'Storage Provider Type', options: { - format: 'dropdown', description: 'Select the cloud storage provider to use for this remote.', }, }, @@ -132,7 +129,8 @@ function getBasicConfigSlice(providerTypes: string[]): SettingSlice { text: 'Documentation', options: { format: 'documentation', - description: 'For more information, refer to the RClone Config Documentation.', + description: + 'For more information, refer to the RClone Config Documentation.', }, }, ]; @@ -151,7 +149,7 @@ function getBasicConfigSlice(providerTypes: string[]): SettingSlice { type: 'string', title: 'Provider Type', default: providerTypes.length > 0 ? providerTypes[0] : '', - oneOf: providerTypes.map(type => ({ const: type, title: type })) + enum: providerTypes, }, }; @@ -162,74 +160,82 @@ function getBasicConfigSlice(providerTypes: string[]): SettingSlice { } /** - * Step 2: Provider-specific configuration based on the selected provider + * Step 2/3: Provider-specific configuration based on the selected provider and whether to show advanced options + * + * @param selectedProvider - The selected provider type + * @param providerOptions - The provider options for the selected provider */ function getProviderConfigSlice( selectedProvider: string, - providerOptions: Record + providerOptions: RCloneProviderOptionResponse[], + showAdvancedOptions: boolean = false ): SettingSlice { // Default elements for when a provider isn't selected or options aren't loaded - let providerConfigElements: UIElement[] = [ + let configElements: UIElement[] = [ { type: 'Label', - text: 'Provider Configuration', + text: `${showAdvancedOptions ? 'Advanced' : 'Provider'} Configuration`, options: { - format: 'loading', - description: 'Select a provider type first to see provider-specific options.', + format: 'loading', // Or 'note' if preferred + description: `Select a provider type first to see ${showAdvancedOptions ? 'advanced' : 'standard'} options.`, }, }, ]; // Default properties when no provider is selected - let providerConfigProperties: Record = {}; + let configProperties: Record = {}; - // If we have a selected provider and options for it - if (selectedProvider && Object.keys(providerOptions).length > 0) { - // Create dynamic UI elements based on provider options - providerConfigElements = Object.entries(providerOptions).map(([key, option]) => { - if (option.Advanced === true) { - return null; // Skip advanced options for this step - } + if (!selectedProvider || providerOptions.length === 0) { + return { + properties: configProperties as unknown as DataSlice, + elements: [], + }; + } - return { - type: 'Control', - scope: `#/properties/parameters/properties/${key}`, - label: option.Help || key, - options: { - placeholder: option.Default?.toString() || '', - description: option.Help || '', - required: option.Required || false, - format: getFormatForType(option.Type, option.Options), - hide: option.Hide === 1, - }, - }; - }).filter(Boolean) as UIElement[]; - - // No options available case - if (providerConfigElements.length === 0) { - providerConfigElements = [ - { - type: 'Label', - text: 'No Configuration Required', - options: { - description: 'This provider does not require additional configuration, or all options are advanced.', - }, - }, - ]; + // Filter options based on the showAdvancedOptions flag + const filteredOptions = providerOptions.filter((option) => { + if (showAdvancedOptions && option.Advanced === true) { + return true; + } else if (!showAdvancedOptions && option.Advanced !== true) { + return true; } + return false; + }); - // Create dynamic properties schema based on provider options - const paramProperties: Record = {}; - - Object.entries(providerOptions).forEach(([key, option]) => { - if (option.Advanced === true) { - return; // Skip advanced options for this step - } + // Create dynamic UI elements based on filtered provider options + const elements = filteredOptions.map((option) => { + return { + type: 'Control', + scope: `#/properties/parameters/properties/${option.Name}`, + label: option.Help || option.Name, // Use Help as primary label if available + options: { + placeholder: option.Default?.toString() || '', + description: option.Help || '', // Redundant? Keep for potential differences + required: option.Required || false, + format: getJsonFormElementForType( + option.Type, + option.Examples?.map((example) => example.Value), + option.IsPassword + ), + hide: option.Hide === 1, + }, + }; + }); - paramProperties[key] = translateRCloneOptionToJsonSchema(option); - }); + // Create dynamic properties schema based on filtered provider options + const paramProperties: Record = {}; + filteredOptions.forEach((option) => { + if (option) { + // Ensure option exists before translating + paramProperties[option.Name] = translateRCloneOptionToJsonSchema(option); + } + }); - providerConfigProperties = { + console.log('paramProperties', paramProperties); + + // Only add parameters object if there are properties + if (Object.keys(paramProperties).length > 0) { + configProperties = { parameters: { type: 'object', properties: paramProperties, @@ -238,85 +244,8 @@ function getProviderConfigSlice( } return { - properties: providerConfigProperties as unknown as DataSlice, - elements: providerConfigElements, - }; -} - -/** - * Step 3: Advanced configuration options for the selected provider - */ -function getAdvancedConfigSlice( - selectedProvider: string, - providerOptions: Record -): SettingSlice { - // Default elements when no advanced options are available - let advancedConfigElements: UIElement[] = [ - { - type: 'Label', - text: 'Advanced Configuration', - options: { - format: 'note', - description: 'No advanced options available for this provider.', - }, - }, - ]; - - // Default properties - let advancedConfigProperties: Record = {}; - - // If we have a selected provider and options - if (selectedProvider && Object.keys(providerOptions).length > 0) { - // Create dynamic UI elements for advanced options - const advancedElements = Object.entries(providerOptions).map(([key, option]) => { - if (!option.Advanced) { - return null; // Skip non-advanced options - } - - return { - type: 'Control', - scope: `#/properties/parameters/properties/${key}`, - label: option.Help || key, - options: { - placeholder: option.Default?.toString() || '', - description: option.Help || '', - required: option.Required || false, - format: getFormatForType(option.Type, option.Options), - hide: option.Hide === 1, - }, - }; - }).filter(Boolean) as UIElement[]; - - // Use default message if no advanced options - if (advancedElements.length > 0) { - advancedConfigElements = advancedElements; - } - - // Create dynamic properties schema for advanced options - const advancedProperties: Record = {}; - - Object.entries(providerOptions).forEach(([key, option]) => { - if (!option.Advanced) { - return; // Skip non-advanced options - } - - advancedProperties[key] = translateRCloneOptionToJsonSchema(option); - }); - - // Only add if we have advanced options - if (Object.keys(advancedProperties).length > 0) { - advancedConfigProperties = { - parameters: { - type: 'object', - properties: advancedProperties, - }, - }; - } - } - - return { - properties: advancedConfigProperties as unknown as DataSlice, - elements: advancedConfigElements, + properties: configProperties, + elements, }; } @@ -326,14 +255,21 @@ function getAdvancedConfigSlice( function getJsonSchemaType(rcloneType: string): string { switch (rcloneType?.toLowerCase()) { case 'int': - case 'size': - case 'duration': + return 'integer'; // Use 'integer' for whole numbers + case 'size': // Assuming 'size' might imply large numbers, but 'number' is safer if decimals possible + case 'number': // If rclone explicitly uses 'number' return 'number'; + case 'sizesuffix': + case 'duration': + // Represent these as strings, validation handled by pattern/format + return 'string'; case 'bool': return 'boolean'; case 'string': - case 'text': + case 'text': // Treat 'text' (multi-line) as 'string' in schema type + case 'password': // Passwords are strings default: + // Default to string if type is unknown or not provided return 'string'; } } @@ -341,25 +277,37 @@ function getJsonSchemaType(rcloneType: string): string { /** * Helper function to get the appropriate UI format based on RClone option type */ -function getFormatForType(rcloneType: string = '', options: string[] | null = null): string { +function getJsonFormElementForType( + rcloneType: string = '', + options: string[] | null = null, + isPassword: boolean = false +): string | undefined { + if (isPassword) { + return 'password'; + } if (options && options.length > 0) { - return 'dropdown'; + return 'dropdown'; // Use enum for dropdowns } switch (rcloneType?.toLowerCase()) { case 'int': + return 'number'; // Use NumberField case 'size': - return 'number'; + return 'number'; // Use NumberField + case 'sizesuffix': + return undefined; // Use default InputField (via isStringControl) case 'duration': - return 'duration'; + return undefined; // Use default InputField (via isStringControl) case 'bool': - return 'checkbox'; - case 'password': - return 'password'; + return 'checkbox'; // Matches Switch.vue if toggle=true, else default bool render case 'text': - return 'textarea'; + // Consider 'textarea' format later if needed + return undefined; // Use default InputField (via isStringControl) + case 'password': + return 'password'; // Explicit format for password managers etc. + case 'string': default: - return 'text'; + return undefined; // Use default InputField (via isStringControl) } } @@ -373,7 +321,8 @@ export function getRcloneConfigSlice(): SettingSlice { text: 'Configure RClone Backup', options: { format: 'title', - description: 'This 3-step process will guide you through setting up your RClone backup configuration.', + description: + 'This 3-step process will guide you through setting up your RClone backup configuration.', }, }, { @@ -400,7 +349,7 @@ export function getRcloneConfigSlice(): SettingSlice { effect: RuleEffect.SHOW, condition: { scope: '#/properties/configStep', - schema: { enum: [1] } // Only show on step 2 + schema: { enum: [1] }, // Only show on step 2 } as SchemaBasedCondition, }, }, diff --git a/api/src/unraid-api/graph/resolvers/rclone/rclone-api.service.ts b/api/src/unraid-api/graph/resolvers/rclone/rclone-api.service.ts index 30aa83baf..fd963d470 100644 --- a/api/src/unraid-api/graph/resolvers/rclone/rclone-api.service.ts +++ b/api/src/unraid-api/graph/resolvers/rclone/rclone-api.service.ts @@ -4,9 +4,18 @@ import { existsSync } from 'node:fs'; import { mkdir, writeFile } from 'node:fs/promises'; import { dirname, join } from 'node:path'; + + import { execa } from 'execa'; import got from 'got'; -import { RCloneProvider, RCloneProviderOption, RCloneProviderOptionExample, RCloneProviderResponse, RCloneProviderOptionResponse, RCloneProviderTypes } from '@app/unraid-api/graph/resolvers/rclone/rclone.model.js'; + + + +import { RCloneProvider, RCloneProviderOption, RCloneProviderOptionExample, RCloneProviderOptionResponse, RCloneProviderResponse, RCloneProviderTypes } from '@app/unraid-api/graph/resolvers/rclone/rclone.model.js'; + + + + @Injectable() export class RCloneApiService implements OnModuleInit, OnModuleDestroy { @@ -103,23 +112,9 @@ export class RCloneApiService implements OnModuleInit, OnModuleDestroy { /** * Get providers supported by RClone */ - async getProviders(): Promise { + async getProviders(): Promise { const response = await this.callRcloneApi('config/providers') as { providers: RCloneProviderResponse[] }; - return response?.providers?.map(provider => ({ - name: provider.Name, - description: provider.Description, - prefix: provider.Prefix, - options: this.mapProviderOptions(provider.Options), - })) || []; - } - - /** - * Get provider types as a simple array - */ - async getProviderTypes(): Promise { - const providers = await this.getProviders(); - const types: string[] = providers.map(provider => provider.prefix); - return { types }; + return response?.providers || []; } /** @@ -269,4 +264,4 @@ export class RCloneApiService implements OnModuleInit, OnModuleDestroy { password: this.rclonePassword, }; } -} +} \ No newline at end of file diff --git a/api/src/unraid-api/graph/resolvers/rclone/rclone-config.resolver.ts b/api/src/unraid-api/graph/resolvers/rclone/rclone-config.resolver.ts deleted file mode 100644 index 140de170f..000000000 --- a/api/src/unraid-api/graph/resolvers/rclone/rclone-config.resolver.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { Args, Context, Mutation, Parent, ResolveField, Resolver } from '@nestjs/graphql'; -import { Logger } from '@nestjs/common'; - -import { Layout } from '@jsonforms/core'; -import { GraphQLJSON } from 'graphql-scalars'; - -import { - AuthActionVerb, - AuthPossession, - UsePermissions, -} from '@app/unraid-api/graph/directives/use-permissions.directive.js'; -import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js'; -import { RCloneBackupConfigForm, RCloneRemote, CreateRCloneRemoteInput } from '@app/unraid-api/graph/resolvers/rclone/rclone.model.js'; -import { RCloneService } from '@app/unraid-api/graph/resolvers/rclone/rclone.service.js'; -import { RCloneApiService } from '@app/unraid-api/graph/resolvers/rclone/rclone-api.service.js'; -import { DataSlice } from '@app/unraid-api/types/json-forms.js'; -import { RCloneFormService } from '@app/unraid-api/graph/resolvers/rclone/rclone-form.service.js'; - -@Resolver(() => RCloneBackupConfigForm) -export class RCloneConfigResolver { - private readonly logger = new Logger(RCloneConfigResolver.name); - - constructor( - private readonly rcloneService: RCloneService, - private readonly rcloneApiService: RCloneApiService, - private readonly rcloneFormService: RCloneFormService - ) {} - - @ResolveField(() => GraphQLJSON) - async dataSchema( - @Parent() _parent: RCloneBackupConfigForm, - @Args('providerType', { nullable: true }) argProviderType?: string, - @Args('parameters', { type: () => GraphQLJSON, nullable: true }) argParameters?: Record, - @Context() context?: any - ): Promise<{ properties: DataSlice; type: 'object' }> { - try { - // Get providerType and parameters from parent query if not provided directly - let providerType = argProviderType || ''; - let parameters = argParameters || {}; - - // Check for these values in the parent query context - if (context?.variableValues) { - providerType = providerType || context.variableValues.providerType || ''; - parameters = parameters || context.variableValues.parameters || {}; - } - - this.logger.debug(`dataSchema using providerType: ${providerType}, parameters: ${JSON.stringify(parameters)}`); - - // Get provided types - let providerTypes: string[] = []; - try { - const providers = await this.rcloneApiService.getProviderTypes(); - providerTypes = providers.types; - } catch (error) { - this.logger.warn(`Could not get provider types for dataSchema: ${error}`); - } - - // Get the schema with provider information - const schema = await this.rcloneFormService.dataSchema( - providerTypes, - providerType, - parameters - ); - - return { - properties: schema.properties as DataSlice, - type: 'object', - }; - } catch (error) { - this.logger.error(`Error generating dataSchema: ${error}`); - return { - properties: {} as DataSlice, - type: 'object', - }; - } - } - - @ResolveField(() => GraphQLJSON) - async uiSchema( - @Parent() _parent: RCloneBackupConfigForm, - @Args('providerType', { nullable: true }) argProviderType?: string, - @Args('parameters', { type: () => GraphQLJSON, nullable: true }) argParameters?: Record, - @Context() context?: any - ): Promise { - try { - // Get providerType and parameters from parent query if not provided directly - let providerType = argProviderType || ''; - let parameters = argParameters || {}; - - // Check for these values in the parent query context - if (context?.variableValues) { - providerType = providerType || context.variableValues.providerType || ''; - parameters = parameters || context.variableValues.parameters || {}; - } - - this.logger.debug(`uiSchema using providerType: ${providerType}, parameters: ${JSON.stringify(parameters)}`); - - // Get provided types - let providerTypes: string[] = []; - try { - const providers = await this.rcloneApiService.getProviderTypes(); - providerTypes = providers.types; - } catch (error) { - this.logger.warn(`Could not get provider types for uiSchema: ${error}`); - } - - return this.rcloneFormService.uiSchema( - providerTypes, - providerType, - parameters - ); - } catch (error) { - this.logger.error(`Error generating uiSchema: ${error}`); - return { - type: 'VerticalLayout', - elements: [], - }; - } - } - - @Mutation(() => RCloneRemote) - @UsePermissions({ - action: AuthActionVerb.CREATE, - resource: Resource.FLASH, - possession: AuthPossession.ANY, - }) - async createRCloneRemote(@Args('input') input: CreateRCloneRemoteInput): Promise { - try { - await this.rcloneApiService.createRemote(input.name, input.type, input.config); - return { - name: input.name, - type: input.type, - config: input.config - }; - } catch (error) { - this.logger.error(`Error creating remote: ${error}`); - throw new Error(`Failed to create remote: ${error}`); - } - } -} diff --git a/api/src/unraid-api/graph/resolvers/rclone/rclone-form.service.ts b/api/src/unraid-api/graph/resolvers/rclone/rclone-form.service.ts index 15815c3b8..a101e99cb 100644 --- a/api/src/unraid-api/graph/resolvers/rclone/rclone-form.service.ts +++ b/api/src/unraid-api/graph/resolvers/rclone/rclone-form.service.ts @@ -1,28 +1,45 @@ import { Injectable, Logger } from '@nestjs/common'; + + + import { type Layout } from '@jsonforms/core'; + + import type { SettingSlice } from '@app/unraid-api/types/json-forms.js'; +import { RCloneProviderOptionResponse, RCloneProviderResponse } from '@app/unraid-api/graph/resolvers/rclone/rclone.model.js'; import { mergeSettingSlices } from '@app/unraid-api/types/json-forms.js'; -import { - getRcloneConfigFormSchema, - getRcloneConfigSlice, - getAvailableProviderTypes, -} from './jsonforms/rclone-jsonforms-config.js'; + + +import { getRcloneConfigFormSchema, getRcloneConfigSlice } from './jsonforms/rclone-jsonforms-config.js'; import { RCloneApiService } from './rclone-api.service.js'; + +/** + * Field metadata interface for form generation + */ +interface FieldMetadata { + name: string; + label: string; + type: string; + required: boolean; + default: any; + examples: any[]; + options: string[]; + validationType: string; +} + /** * Service responsible for generating form UI schemas and form logic */ @Injectable() export class RCloneFormService { private readonly logger = new Logger(RCloneFormService.name); - private _providerTypes: string[] = []; - private _providerOptions: Record = {}; + private providerNames: string[] = []; + private providerOptions: Record = {}; - constructor( - private readonly rcloneApiService: RCloneApiService - ) {} + constructor(private readonly rcloneApiService: RCloneApiService) {} /** * Loads RClone provider types and options @@ -32,12 +49,12 @@ export class RCloneFormService { const providersResponse = await this.rcloneApiService.getProviders(); if (providersResponse) { // Extract provider types - this._providerTypes = providersResponse.map(provider => provider.name); - this._providerOptions = providersResponse.reduce((acc, provider) => { - acc[provider.name] = provider.options; + this.providerNames = providersResponse.map((provider) => provider.Name); + this.providerOptions = providersResponse.reduce((acc, provider) => { + acc[provider.Name] = provider.Options; return acc; }, {}); - this.logger.debug(`Loaded ${this._providerTypes.length} provider types`); + this.logger.debug(`Loaded ${this.providerNames.length} provider types`); } } catch (error) { this.logger.error(`Error loading provider information: ${error}`); @@ -45,74 +62,244 @@ export class RCloneFormService { } } + /** + * Determines the appropriate input type for a parameter based on its configuration + * Following the logic from rclone-webui-react + */ + private getInputType(attr: any): string { + if (attr.IsPassword) { + return 'password'; + } else if (!this.isEmpty(attr.Examples)) { + return 'string'; + } else if (attr.Type === 'bool') { + return 'select'; + } else if (attr.Type === 'int') { + return 'number'; + } else if (attr.Type === 'string') { + return 'text'; + } else if (attr.Type === 'SizeSuffix') { + return 'string'; // Special validation will be applied + } else if (attr.Type === 'Duration') { + return 'string'; // Special validation will be applied + } else { + return 'text'; + } + } + + /** + * Helper method to check if a value is empty (null, undefined, or empty array/object) + * Following the logic from rclone-webui-react + */ + private isEmpty(value: any): boolean { + if (value === null || value === undefined) { + return true; + } + if (Array.isArray(value) && value.length === 0) { + return true; + } + if (typeof value === 'object' && Object.keys(value).length === 0) { + return true; + } + return false; + } + + /** + * Validates size suffix format (e.g., "10G", "100M") + * Following the logic from rclone-webui-react + */ + private validateSizeSuffix(value: string): boolean { + if (value === 'off' || value === '') return true; + const regex = /^(\d+)([KMGT])?$/i; + return regex.test(value); + } + + /** + * Validates duration format (e.g., "10ms", "1h30m") + * Following the logic from rclone-webui-react + */ + private validateDuration(value: string): boolean { + if (value === '' || value === 'off') return true; + const regex = /^((\d+)h)?((\d+)m)?((\d+)s)?((\d+)ms)?$/i; + return regex.test(value); + } + + /** + * Validates integer format + * Following the logic from rclone-webui-react + */ + private validateInt(value: string): boolean { + if (value === '') return true; + const regex = /^\d+$/; + return regex.test(value); + } + + /** + * Process provider options to create form fields + * Following the logic from rclone-webui-react + */ + private processProviderOptions( + providerOptions: RCloneProviderOptionResponse[], + loadAdvanced: boolean = false + ): { + fields: FieldMetadata[]; + required: Record; + optionTypes: Record; + } { + if (!providerOptions) { + return { fields: [], required: {}, optionTypes: {} }; + } + + const fields: FieldMetadata[] = []; + const required: Record = {}; + const optionTypes: Record = {}; + + for (const attr of providerOptions) { + // Skip hidden options and respect advanced flag + if ( + attr.Hide === 0 && + ((loadAdvanced && attr.Advanced) || (!loadAdvanced && !attr.Advanced)) + ) { + const inputType = this.getInputType(attr); + + // Build field metadata + const field: FieldMetadata = { + name: attr.Name, + label: attr.Help || attr.Name, + type: inputType, + required: attr.Required || false, + default: attr.DefaultStr || attr.Default, + examples: attr.Examples || [], + options: attr.Type === 'bool' ? ['Yes', 'No'] : [], + validationType: attr.Type || '', + }; + + fields.push(field); + + // Track required fields + if (attr.Required) { + required[attr.Name] = true; + } + + // Track option types for validation + optionTypes[attr.Name] = attr.Type ?? ''; + } + } + + return { fields, required, optionTypes }; + } + /** * Builds the complete settings schema + * Modified to support both standard and advanced options */ async buildSettingsSchema( - providerTypes: string[] = [], selectedProvider: string = '', - providerOptions: Record = {} + loadAdvanced: boolean = false ): Promise { + // Ensure provider info is loaded + if (Object.keys(this.providerOptions).length === 0) { + await this.loadProviderInfo(); + } + // Get the stepper UI and the form based on the selected provider const baseSlice = getRcloneConfigSlice(); + this.logger.debug('Getting rclone form schema for provider: ' + selectedProvider); + + // Check if the form schema generator supports the loadAdvanced parameter + // If not, we'll handle the advanced filtering in our own logic const formSlice = getRcloneConfigFormSchema( - providerTypes, + this.providerNames, selectedProvider, - providerOptions + this.providerOptions ); return mergeSettingSlices([baseSlice, formSlice]); } /** - * Returns the data schema for the form + * Returns both data schema and UI schema for the form + * Now supports advanced options toggle */ - async dataSchema( - providerTypes: string[] = [], + async getFormSchemas( selectedProvider: string = '', - providerOptions: Record = {} - ): Promise> { - // If provider types weren't provided and we haven't loaded them yet, try to load them - if (providerTypes.length === 0 && this._providerTypes.length === 0) { + loadAdvanced: boolean = false + ): Promise<{ + dataSchema: Record; + uiSchema: Layout; + validationInfo: { + required: Record; + optionTypes: Record; + }; + }> { + // Ensure provider info is loaded + if (Object.keys(this.providerOptions).length === 0) { await this.loadProviderInfo(); - providerTypes = this._providerTypes; - providerOptions = this._providerOptions; } - - const { properties } = await this.buildSettingsSchema( - providerTypes, - selectedProvider, - providerOptions - ); + + const { properties, elements } = await this.buildSettingsSchema(selectedProvider, loadAdvanced); + + // Process provider options to get validation information + const providerOpts = selectedProvider ? this.providerOptions[selectedProvider] : []; + const { required, optionTypes } = this.processProviderOptions(providerOpts, loadAdvanced); + return { - type: 'object', - properties, + dataSchema: { + type: 'object', + properties, + }, + uiSchema: { + type: 'VerticalLayout', + elements, + }, + validationInfo: { + required, + optionTypes, + }, }; } /** - * Returns the UI schema for the form + * Validates form values based on their types + * Following the logic from rclone-webui-react */ - async uiSchema( - providerTypes: string[] = [], - selectedProvider: string = '', - providerOptions: Record = {} - ): Promise { - // If provider types weren't provided and we haven't loaded them yet, try to load them - if (providerTypes.length === 0 && this._providerTypes.length === 0) { - await this.loadProviderInfo(); - providerTypes = this._providerTypes; - providerOptions = this._providerOptions; + validateFormValues( + values: Record, + optionTypes: Record + ): Record { + const result: Record = {}; + + for (const [key, value] of Object.entries(values)) { + const inputType = optionTypes[key]; + let isValid = true; + let error = ''; + + // Skip if no value or no type + if (!value || !inputType) { + result[key] = { valid: true, error: '' }; + continue; + } + + // Validate based on type + if (inputType === 'SizeSuffix') { + isValid = this.validateSizeSuffix(value as string); + if (!isValid) { + error = 'Valid format: off | {number}{unit} (e.g., 10G, 100M)'; + } + } else if (inputType === 'Duration') { + isValid = this.validateDuration(value as string); + if (!isValid) { + error = 'Valid format: {number}{unit} (e.g., 10ms, 1h30m)'; + } + } else if (inputType === 'int') { + isValid = this.validateInt(value as string); + if (!isValid) { + error = 'Must be a valid integer'; + } + } + + result[key] = { valid: isValid, error }; } - - const { elements } = await this.buildSettingsSchema( - providerTypes, - selectedProvider, - providerOptions - ); - return { - type: 'VerticalLayout', - elements, - }; + + return result; } -} \ No newline at end of file +} \ No newline at end of file diff --git a/api/src/unraid-api/graph/resolvers/rclone/rclone.model.ts b/api/src/unraid-api/graph/resolvers/rclone/rclone.model.ts index 58115f69d..ef6afcf32 100644 --- a/api/src/unraid-api/graph/resolvers/rclone/rclone.model.ts +++ b/api/src/unraid-api/graph/resolvers/rclone/rclone.model.ts @@ -38,7 +38,7 @@ export interface RCloneProviderOptionResponse { Default?: unknown; Value?: unknown; ShortOpt?: string; - Hide?: boolean; + Hide?: number; Required?: boolean; IsPassword?: boolean; NoPrefix?: boolean; @@ -49,99 +49,6 @@ export interface RCloneProviderOptionResponse { Examples?: Array<{ Value: string; Help: string; Provider: string }>; } -@ObjectType() -export class RCloneProviderOption { - @Field(() => String) - name!: string; - - @Field(() => String) - help!: string; - - @Field(() => String) - provider!: string; - - @Field(() => GraphQLJSON, { nullable: true }) - default?: unknown; - - @Field(() => GraphQLJSON, { nullable: true }) - value?: unknown; - - @Field(() => String, { nullable: true }) - shortOpt?: string; - - @Field(() => Boolean, { nullable: true }) - hide?: boolean; - - @Field(() => Boolean, { nullable: true }) - required?: boolean; - - @Field(() => Boolean, { nullable: true }) - isPassword?: boolean; - - @Field(() => Boolean, { nullable: true }) - noPrefix?: boolean; - - @Field(() => Boolean, { nullable: true }) - advanced?: boolean; - - @Field(() => String, { nullable: true }) - defaultStr?: string; - - @Field(() => String, { nullable: true }) - valueStr?: string; - - @Field(() => String, { nullable: true }) - type?: string; - - @Field(() => [RCloneProviderOptionExample], { nullable: true }) - examples?: RCloneProviderOptionExample[]; -} - -@ObjectType() -export class RCloneProviderOptionExample { - @Field(() => String) - value!: string; - - @Field(() => String) - help!: string; - - @Field(() => String) - provider!: string; -} - -@ObjectType() -export class RCloneProviderTypes { - @Field(() => [String], { description: 'List of all provider types' }) - types!: string[]; -} - -/** - * { - Name: 'jottacloud', - Description: 'Jottacloud', - Prefix: 'jottacloud', - Options: [Array], - CommandHelp: null, - Aliases: null, - Hide: false, - MetadataInfo: [Object] - }, - */ -@ObjectType() -export class RCloneProvider { - @Field(() => String) - name!: string; - - @Field(() => String) - description!: string; - - @Field(() => String) - prefix!: string; - - @Field(() => [RCloneProviderOption]) - options!: RCloneProviderOption[]; -} - @ObjectType() export class RCloneBackupConfigForm { @Field(() => ID) @@ -152,6 +59,12 @@ export class RCloneBackupConfigForm { @Field(() => GraphQLJSON) uiSchema!: Layout; + + @Field(() => String, { nullable: true }) + providerType?: string; + + @Field(() => GraphQLJSON, { nullable: true }) + parameters?: Record; } @ObjectType() @@ -175,7 +88,7 @@ export class RCloneRemote { type!: string; @Field(() => GraphQLJSON) - config!: Record; + parameters!: Record; } @InputType() diff --git a/api/src/unraid-api/graph/resolvers/rclone/rclone.module.ts b/api/src/unraid-api/graph/resolvers/rclone/rclone.module.ts index 601fe7b82..8f1071cd5 100644 --- a/api/src/unraid-api/graph/resolvers/rclone/rclone.module.ts +++ b/api/src/unraid-api/graph/resolvers/rclone/rclone.module.ts @@ -4,7 +4,6 @@ import { RCloneService } from '@app/unraid-api/graph/resolvers/rclone/rclone.ser import { RCloneApiService } from '@app/unraid-api/graph/resolvers/rclone/rclone-api.service.js'; import { RCloneFormService } from '@app/unraid-api/graph/resolvers/rclone/rclone-form.service.js'; import { RCloneBackupSettingsResolver } from '@app/unraid-api/graph/resolvers/rclone/rclone.resolver.js'; -import { RCloneConfigResolver } from '@app/unraid-api/graph/resolvers/rclone/rclone-config.resolver.js'; @Module({ imports: [], @@ -13,7 +12,6 @@ import { RCloneConfigResolver } from '@app/unraid-api/graph/resolvers/rclone/rcl RCloneApiService, RCloneFormService, RCloneBackupSettingsResolver, - RCloneConfigResolver ], exports: [RCloneService, RCloneApiService] }) diff --git a/api/src/unraid-api/graph/resolvers/rclone/rclone.resolver.ts b/api/src/unraid-api/graph/resolvers/rclone/rclone.resolver.ts index a1a819bb1..82e12fdee 100644 --- a/api/src/unraid-api/graph/resolvers/rclone/rclone.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/rclone/rclone.resolver.ts @@ -1,22 +1,22 @@ import { Logger } from '@nestjs/common'; -import { Args, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql'; +import { Args, Mutation, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql'; + + + import { GraphQLJSON } from 'graphql-scalars'; -import { - AuthActionVerb, - AuthPossession, - UsePermissions, -} from '@app/unraid-api/graph/directives/use-permissions.directive.js'; + + +import { AuthActionVerb, AuthPossession, UsePermissions } from '@app/unraid-api/graph/directives/use-permissions.directive.js'; import { Resource } from '@app/unraid-api/graph/resolvers/base.model.js'; import { RCloneApiService } from '@app/unraid-api/graph/resolvers/rclone/rclone-api.service.js'; -import { - RCloneBackupSettings, - RCloneBackupConfigForm, - RCloneDrive, -} from '@app/unraid-api/graph/resolvers/rclone/rclone.model.js'; +import { CreateRCloneRemoteInput, RCloneBackupConfigForm, RCloneBackupSettings, RCloneRemote } from '@app/unraid-api/graph/resolvers/rclone/rclone.model.js'; + + -import { RCloneService } from './rclone.service.js'; import { RCloneFormService } from './rclone-form.service.js'; +import { RCloneService } from './rclone.service.js'; + @Resolver(() => RCloneBackupSettings) export class RCloneBackupSettingsResolver { @@ -40,28 +40,40 @@ export class RCloneBackupSettingsResolver { @ResolveField(() => RCloneBackupConfigForm) async configForm( - @Parent() _parent: RCloneBackupSettings, - @Args('providerType', { nullable: true }) providerType?: string, - @Args('parameters', { type: () => GraphQLJSON, nullable: true }) parameters?: Record + @Parent() _parent: RCloneBackupSettings, + @Args('providerType', { nullable: true }) providerType?: string, + @Args('parameters', { type: () => GraphQLJSON, nullable: true }) + parameters?: Record ): Promise { - // Return basic form info without generating schema data - this will be handled by RCloneConfigResolver + // Return form info with the provided arguments + + const form = await this.rcloneFormService.getFormSchemas(providerType); return { id: 'rcloneBackupConfigForm', + dataSchema: form.dataSchema, + uiSchema: form.uiSchema, + providerType, + parameters, } as RCloneBackupConfigForm; } - @ResolveField(() => [RCloneDrive]) - async drives(@Parent() _parent: RCloneBackupSettings): Promise { + @Mutation(() => RCloneRemote) + @UsePermissions({ + action: AuthActionVerb.CREATE, + resource: Resource.FLASH, + possession: AuthPossession.ANY, + }) + async createRCloneRemote(@Args('input') input: CreateRCloneRemoteInput): Promise { try { - const providers = await this.rcloneApiService.getProviders(); - - return providers.map(provider => ({ - name: provider.name, - options: provider.options as unknown as Record, - })); + await this.rcloneApiService.createRemote(input.name, input.type, input.parameters); + return { + name: input.name, + type: input.type, + parameters: input.parameters, + }; } catch (error) { - this.logger.error(`Error getting providers: ${error}`); - return []; + this.logger.error(`Error creating remote: ${error}`); + throw new Error(`Failed to create remote: ${error}`); } } @@ -74,4 +86,4 @@ export class RCloneBackupSettingsResolver { return []; } } -} +} \ No newline at end of file diff --git a/unraid-ui/src/forms/InputField.vue b/unraid-ui/src/forms/InputField.vue new file mode 100644 index 000000000..f9b59bcae --- /dev/null +++ b/unraid-ui/src/forms/InputField.vue @@ -0,0 +1,36 @@ + + + diff --git a/unraid-ui/src/forms/renderer-entries.ts b/unraid-ui/src/forms/renderer-entries.ts index dfa169893..eebad6bde 100644 --- a/unraid-ui/src/forms/renderer-entries.ts +++ b/unraid-ui/src/forms/renderer-entries.ts @@ -3,6 +3,7 @@ import PreconditionsLabel from '@/forms/PreconditionsLabel.vue'; import selectRenderer from '@/forms/Select.vue'; import StringArrayField from '@/forms/StringArrayField.vue'; import switchRenderer from '@/forms/Switch.vue'; +import inputFieldRenderer from '@/forms/InputField.vue'; import { and, isBooleanControl, @@ -10,6 +11,7 @@ import { isEnumControl, isIntegerControl, isNumberControl, + isStringControl, optionIs, or, rankWith, @@ -39,6 +41,11 @@ export const numberFieldEntry: JsonFormsRendererRegistryEntry = { tester: rankWith(4, or(isNumberControl, isIntegerControl)), }; +export const inputFieldEntry: JsonFormsRendererRegistryEntry = { + renderer: inputFieldRenderer, + tester: rankWith(3, isStringControl), +}; + export const stringArrayEntry: JsonFormsRendererRegistryEntry = { renderer: StringArrayField, tester: rankWith(4, and(isControl, schemaMatches(isStringArray))), diff --git a/unraid-ui/src/forms/renderers.ts b/unraid-ui/src/forms/renderers.ts index 9364f7cc7..277785115 100644 --- a/unraid-ui/src/forms/renderers.ts +++ b/unraid-ui/src/forms/renderers.ts @@ -1,6 +1,7 @@ import { formSelectEntry, formSwitchEntry, + inputFieldEntry, numberFieldEntry, preconditionsLabelEntry, stringArrayEntry, @@ -18,6 +19,7 @@ export const jsonFormsRenderers = [ ...vanillaRenderers, formSwitchEntry, formSelectEntry, + inputFieldEntry, numberFieldEntry, preconditionsLabelEntry, stringArrayEntry, diff --git a/web/components/RClone/RCloneConfig.vue b/web/components/RClone/RCloneConfig.vue index 9d8621478..d4d9f2742 100644 --- a/web/components/RClone/RCloneConfig.vue +++ b/web/components/RClone/RCloneConfig.vue @@ -2,7 +2,7 @@ import { computed, ref, watch } from 'vue'; import { useMutation, useQuery } from '@vue/apollo-composable'; -import { BrandButton, jsonFormsRenderers } from '@unraid/ui'; +import { BrandButton, Input, jsonFormsRenderers } from '@unraid/ui'; import { JsonForms } from '@jsonforms/vue'; import type { CreateRCloneRemoteInput } from '~/composables/gql/graphql'; @@ -31,15 +31,18 @@ const { result: formResult, loading: formLoading, refetch: updateFormSchema, -} = useQuery(GET_RCLONE_CONFIG_FORM, () => ({ +} = useQuery(GET_RCLONE_CONFIG_FORM, { providerType: formState.value.type, parameters: formState.value.parameters, -})); +}); // Watch for provider type changes to update schema watch(providerType, async (newType) => { if (newType) { - await updateFormSchema(); + await updateFormSchema({ + providerType: newType, + parameters: formState.value.parameters, + }); } }); @@ -48,7 +51,10 @@ watch( () => formState.value.configStep, async (newStep) => { if (newStep > 0) { - await updateFormSchema(); + await updateFormSchema({ + providerType: formState.value.type, + parameters: formState.value.parameters, + }); } } ); diff --git a/web/components/RClone/graphql/settings.query.ts b/web/components/RClone/graphql/settings.query.ts index a893d4052..946550d60 100644 --- a/web/components/RClone/graphql/settings.query.ts +++ b/web/components/RClone/graphql/settings.query.ts @@ -8,10 +8,6 @@ export const GET_RCLONE_CONFIG_FORM = graphql(/* GraphQL */ ` dataSchema uiSchema } - drives { - name - options - } } } `); diff --git a/web/composables/gql/gql.ts b/web/composables/gql/gql.ts index 342521788..35b0e6e6d 100644 --- a/web/composables/gql/gql.ts +++ b/web/composables/gql/gql.ts @@ -36,7 +36,7 @@ type Documents = { "\n mutation RecomputeOverview {\n recalculateOverview {\n archive {\n ...NotificationCountFragment\n }\n unread {\n ...NotificationCountFragment\n }\n }\n }\n": typeof types.RecomputeOverviewDocument, "\n subscription NotificationAddedSub {\n notificationAdded {\n ...NotificationFragment\n }\n }\n": typeof types.NotificationAddedSubDocument, "\n subscription NotificationOverviewSub {\n notificationsOverview {\n archive {\n ...NotificationCountFragment\n }\n unread {\n ...NotificationCountFragment\n }\n }\n }\n": typeof types.NotificationOverviewSubDocument, - "\n query GetRCloneConfigForm($providerType: String, $parameters: JSON) {\n rcloneBackup {\n configForm(providerType: $providerType, parameters: $parameters) { \n dataSchema\n uiSchema\n }\n drives {\n name\n options\n }\n }\n }\n": typeof types.GetRCloneConfigFormDocument, + "\n query GetRCloneConfigForm($providerType: String, $parameters: JSON) {\n rcloneBackup {\n configForm(providerType: $providerType, parameters: $parameters) { \n dataSchema\n uiSchema\n }\n }\n }\n": typeof types.GetRCloneConfigFormDocument, "\n query ListRCloneRemotes {\n rcloneBackup {\n remotes\n }\n }\n": typeof types.ListRCloneRemotesDocument, "\n mutation CreateRCloneRemote($input: CreateRCloneRemoteInput!) {\n createRCloneRemote(input: $input) {\n name\n type\n config\n }\n }\n": typeof types.CreateRCloneRemoteDocument, "\n mutation ConnectSignIn($input: ConnectSignInInput!) {\n connectSignIn(input: $input)\n }\n": typeof types.ConnectSignInDocument, @@ -72,7 +72,7 @@ const documents: Documents = { "\n mutation RecomputeOverview {\n recalculateOverview {\n archive {\n ...NotificationCountFragment\n }\n unread {\n ...NotificationCountFragment\n }\n }\n }\n": types.RecomputeOverviewDocument, "\n subscription NotificationAddedSub {\n notificationAdded {\n ...NotificationFragment\n }\n }\n": types.NotificationAddedSubDocument, "\n subscription NotificationOverviewSub {\n notificationsOverview {\n archive {\n ...NotificationCountFragment\n }\n unread {\n ...NotificationCountFragment\n }\n }\n }\n": types.NotificationOverviewSubDocument, - "\n query GetRCloneConfigForm($providerType: String, $parameters: JSON) {\n rcloneBackup {\n configForm(providerType: $providerType, parameters: $parameters) { \n dataSchema\n uiSchema\n }\n drives {\n name\n options\n }\n }\n }\n": types.GetRCloneConfigFormDocument, + "\n query GetRCloneConfigForm($providerType: String, $parameters: JSON) {\n rcloneBackup {\n configForm(providerType: $providerType, parameters: $parameters) { \n dataSchema\n uiSchema\n }\n }\n }\n": types.GetRCloneConfigFormDocument, "\n query ListRCloneRemotes {\n rcloneBackup {\n remotes\n }\n }\n": types.ListRCloneRemotesDocument, "\n mutation CreateRCloneRemote($input: CreateRCloneRemoteInput!) {\n createRCloneRemote(input: $input) {\n name\n type\n config\n }\n }\n": types.CreateRCloneRemoteDocument, "\n mutation ConnectSignIn($input: ConnectSignInInput!) {\n connectSignIn(input: $input)\n }\n": types.ConnectSignInDocument, @@ -191,7 +191,7 @@ export function graphql(source: "\n subscription NotificationOverviewSub {\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 GetRCloneConfigForm($providerType: String, $parameters: JSON) {\n rcloneBackup {\n configForm(providerType: $providerType, parameters: $parameters) { \n dataSchema\n uiSchema\n }\n drives {\n name\n options\n }\n }\n }\n"): (typeof documents)["\n query GetRCloneConfigForm($providerType: String, $parameters: JSON) {\n rcloneBackup {\n configForm(providerType: $providerType, parameters: $parameters) { \n dataSchema\n uiSchema\n }\n drives {\n name\n options\n }\n }\n }\n"]; +export function graphql(source: "\n query GetRCloneConfigForm($providerType: String, $parameters: JSON) {\n rcloneBackup {\n configForm(providerType: $providerType, parameters: $parameters) { \n dataSchema\n uiSchema\n }\n }\n }\n"): (typeof documents)["\n query GetRCloneConfigForm($providerType: String, $parameters: JSON) {\n rcloneBackup {\n configForm(providerType: $providerType, parameters: $parameters) { \n dataSchema\n uiSchema\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/web/composables/gql/graphql.ts b/web/composables/gql/graphql.ts index 16cb6fe9a..143dc9bcf 100644 --- a/web/composables/gql/graphql.ts +++ b/web/composables/gql/graphql.ts @@ -2082,7 +2082,7 @@ export type GetRCloneConfigFormQueryVariables = Exact<{ }>; -export type GetRCloneConfigFormQuery = { __typename?: 'Query', rcloneBackup: { __typename?: 'RCloneBackupSettings', configForm: { __typename?: 'RCloneBackupConfigForm', dataSchema: any, uiSchema: any }, drives: Array<{ __typename?: 'RCloneDrive', name: string, options: any }> } }; +export type GetRCloneConfigFormQuery = { __typename?: 'Query', rcloneBackup: { __typename?: 'RCloneBackupSettings', configForm: { __typename?: 'RCloneBackupConfigForm', dataSchema: any, uiSchema: any } } }; export type ListRCloneRemotesQueryVariables = Exact<{ [key: string]: never; }>; @@ -2170,7 +2170,7 @@ export const OverviewDocument = {"kind":"Document","definitions":[{"kind":"Opera export const RecomputeOverviewDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RecomputeOverview"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"recalculateOverview"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"archive"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NotificationCountFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"unread"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NotificationCountFragment"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NotificationCountFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NotificationCounts"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"info"}},{"kind":"Field","name":{"kind":"Name","value":"warning"}},{"kind":"Field","name":{"kind":"Name","value":"alert"}}]}}]} as unknown as DocumentNode; export const NotificationAddedSubDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"NotificationAddedSub"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"notificationAdded"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NotificationFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NotificationFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Notification"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"subject"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"importance"}},{"kind":"Field","name":{"kind":"Name","value":"link"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"formattedTimestamp"}}]}}]} as unknown as DocumentNode; export const NotificationOverviewSubDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"NotificationOverviewSub"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"notificationsOverview"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"archive"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NotificationCountFragment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"unread"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"NotificationCountFragment"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NotificationCountFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NotificationCounts"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"info"}},{"kind":"Field","name":{"kind":"Name","value":"warning"}},{"kind":"Field","name":{"kind":"Name","value":"alert"}}]}}]} as unknown as DocumentNode; -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":"providerType"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"parameters"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"JSON"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"rcloneBackup"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configForm"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"providerType"},"value":{"kind":"Variable","name":{"kind":"Name","value":"providerType"}}},{"kind":"Argument","name":{"kind":"Name","value":"parameters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"parameters"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dataSchema"}},{"kind":"Field","name":{"kind":"Name","value":"uiSchema"}}]}},{"kind":"Field","name":{"kind":"Name","value":"drives"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"options"}}]}}]}}]}}]} as unknown as DocumentNode; +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":"providerType"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"parameters"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"JSON"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"rcloneBackup"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"configForm"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"providerType"},"value":{"kind":"Variable","name":{"kind":"Name","value":"providerType"}}},{"kind":"Argument","name":{"kind":"Name","value":"parameters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"parameters"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dataSchema"}},{"kind":"Field","name":{"kind":"Name","value":"uiSchema"}}]}}]}}]}}]} as unknown as DocumentNode; 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":"rcloneBackup"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"remotes"}}]}}]}}]} as unknown as DocumentNode; export const CreateRCloneRemoteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateRCloneRemote"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateRCloneRemoteInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createRCloneRemote"},"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":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"config"}}]}}]}}]} as unknown as DocumentNode; export const ConnectSignInDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ConnectSignIn"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ConnectSignInInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connectSignIn"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode;