mirror of
https://github.com/unraid/api.git
synced 2026-01-21 07:59:41 -06:00
feat: ui working for RClone setup
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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<string, RCloneOptionDef> {
|
||||
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<string, RCloneOptionDef>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the UI schema for RClone remote configuration
|
||||
*/
|
||||
export function getRcloneConfigFormSchema(
|
||||
providerTypes: string[] = [],
|
||||
selectedProvider: string = '',
|
||||
providerOptions: Record<string, RCloneOptionDef> = {}
|
||||
providerOptions: Record<string, RCloneProviderOptionResponse[]> = {}
|
||||
): 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 <a href="https://rclone.org/commands/rclone_config/" target="_blank">RClone Config Documentation</a>.',
|
||||
description:
|
||||
'For more information, refer to the <a href="https://rclone.org/commands/rclone_config/" target="_blank">RClone Config Documentation</a>.',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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<string, RCloneOptionDef>
|
||||
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<string, JsonSchema7> = {};
|
||||
let configProperties: Record<string, JsonSchema7> = {};
|
||||
|
||||
// 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<string, JsonSchema7> = {};
|
||||
|
||||
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<UIElement>((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<string, JsonSchema7> = {};
|
||||
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<string, RCloneOptionDef>
|
||||
): 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<string, JsonSchema7> = {};
|
||||
|
||||
// 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<string, JsonSchema7> = {};
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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<RCloneProvider[]> {
|
||||
async getProviders(): Promise<RCloneProviderResponse[]> {
|
||||
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<RCloneProviderTypes> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<string, unknown>,
|
||||
@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<string, unknown>,
|
||||
@Context() context?: any
|
||||
): Promise<Layout> {
|
||||
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<RCloneRemote> {
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<string, any> = {};
|
||||
private providerNames: string[] = [];
|
||||
private providerOptions: Record<string, RCloneProviderOptionResponse[]> = {};
|
||||
|
||||
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<string, boolean>;
|
||||
optionTypes: Record<string, string>;
|
||||
} {
|
||||
if (!providerOptions) {
|
||||
return { fields: [], required: {}, optionTypes: {} };
|
||||
}
|
||||
|
||||
const fields: FieldMetadata[] = [];
|
||||
const required: Record<string, boolean> = {};
|
||||
const optionTypes: Record<string, string> = {};
|
||||
|
||||
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<string, any> = {}
|
||||
loadAdvanced: boolean = false
|
||||
): Promise<SettingSlice> {
|
||||
// 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<string, any> = {}
|
||||
): Promise<Record<string, any>> {
|
||||
// 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<string, any>;
|
||||
uiSchema: Layout;
|
||||
validationInfo: {
|
||||
required: Record<string, boolean>;
|
||||
optionTypes: Record<string, string>;
|
||||
};
|
||||
}> {
|
||||
// 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<string, any> = {}
|
||||
): Promise<Layout> {
|
||||
// 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<string, any>,
|
||||
optionTypes: Record<string, string>
|
||||
): Record<string, { valid: boolean; error: string }> {
|
||||
const result: Record<string, { valid: boolean; error: string }> = {};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<string, unknown>;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
@@ -175,7 +88,7 @@ export class RCloneRemote {
|
||||
type!: string;
|
||||
|
||||
@Field(() => GraphQLJSON)
|
||||
config!: Record<string, unknown>;
|
||||
parameters!: Record<string, unknown>;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
|
||||
@@ -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]
|
||||
})
|
||||
|
||||
@@ -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<string, unknown>
|
||||
@Parent() _parent: RCloneBackupSettings,
|
||||
@Args('providerType', { nullable: true }) providerType?: string,
|
||||
@Args('parameters', { type: () => GraphQLJSON, nullable: true })
|
||||
parameters?: Record<string, unknown>
|
||||
): Promise<RCloneBackupConfigForm> {
|
||||
// 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<RCloneDrive[]> {
|
||||
@Mutation(() => RCloneRemote)
|
||||
@UsePermissions({
|
||||
action: AuthActionVerb.CREATE,
|
||||
resource: Resource.FLASH,
|
||||
possession: AuthPossession.ANY,
|
||||
})
|
||||
async createRCloneRemote(@Args('input') input: CreateRCloneRemoteInput): Promise<RCloneRemote> {
|
||||
try {
|
||||
const providers = await this.rcloneApiService.getProviders();
|
||||
|
||||
return providers.map(provider => ({
|
||||
name: provider.name,
|
||||
options: provider.options as unknown as Record<string, unknown>,
|
||||
}));
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
36
unraid-ui/src/forms/InputField.vue
Normal file
36
unraid-ui/src/forms/InputField.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { Input } from '@/components/form/input';
|
||||
import ControlLayout from '@/forms/ControlLayout.vue';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { ControlElement } from '@jsonforms/core';
|
||||
import type { RendererProps } from '@jsonforms/vue';
|
||||
import { useJsonFormsControl } from '@jsonforms/vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<RendererProps<ControlElement>>();
|
||||
const { control, handleChange } = useJsonFormsControl(props);
|
||||
|
||||
// Bind the input field's value to JSONForms data
|
||||
const value = computed({
|
||||
get: () => control.value.data ?? control.value.schema.default ?? '',
|
||||
set: (newValue: string) => handleChange(control.value.path, newValue || undefined),
|
||||
});
|
||||
|
||||
const classOverride = computed(() => {
|
||||
return cn(control.value.uischema?.options?.class, {
|
||||
'max-w-[25ch]': control.value.uischema?.options?.format === 'short',
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ControlLayout v-if="control.visible" :label="control.label" :errors="control.errors">
|
||||
<Input
|
||||
v-model="value"
|
||||
:class="classOverride"
|
||||
:disabled="!control.enabled"
|
||||
:required="control.required"
|
||||
:placeholder="control.schema.description"
|
||||
/>
|
||||
</ControlLayout>
|
||||
</template>
|
||||
@@ -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))),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -8,10 +8,6 @@ export const GET_RCLONE_CONFIG_FORM = graphql(/* GraphQL */ `
|
||||
dataSchema
|
||||
uiSchema
|
||||
}
|
||||
drives {
|
||||
name
|
||||
options
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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<RecomputeOverviewMutation, RecomputeOverviewMutationVariables>;
|
||||
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<NotificationAddedSubSubscription, NotificationAddedSubSubscriptionVariables>;
|
||||
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<NotificationOverviewSubSubscription, NotificationOverviewSubSubscriptionVariables>;
|
||||
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<GetRCloneConfigFormQuery, GetRCloneConfigFormQueryVariables>;
|
||||
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<GetRCloneConfigFormQuery, GetRCloneConfigFormQueryVariables>;
|
||||
export const ListRCloneRemotesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ListRCloneRemotes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"rcloneBackup"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"remotes"}}]}}]}}]} as unknown as DocumentNode<ListRCloneRemotesQuery, ListRCloneRemotesQueryVariables>;
|
||||
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<CreateRCloneRemoteMutation, CreateRCloneRemoteMutationVariables>;
|
||||
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<ConnectSignInMutation, ConnectSignInMutationVariables>;
|
||||
|
||||
Reference in New Issue
Block a user