mirror of
https://github.com/unraid/api.git
synced 2026-05-19 15:40:31 -05:00
feat: substantial type cleanup
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
[api]
|
||||
version="4.4.1"
|
||||
version="4.8.0"
|
||||
extraOrigins="https://google.com,https://test.com"
|
||||
[local]
|
||||
sandbox="yes"
|
||||
|
||||
@@ -1,17 +1 @@
|
||||
[
|
||||
{
|
||||
"id": "1ad3f9a9-f438-43b2-bdd5-976c7ca4b2f5",
|
||||
"name": "test",
|
||||
"sourcePath": "/Users/elibosley/Downloads",
|
||||
"remoteName": "FlashBackup",
|
||||
"destinationPath": "backup",
|
||||
"schedule": "0 2 * * *",
|
||||
"enabled": false,
|
||||
"rcloneOptions": {},
|
||||
"createdAt": "2025-05-24T12:19:29.150Z",
|
||||
"updatedAt": "2025-05-26T16:14:13.977Z",
|
||||
"lastRunStatus": "Started with job ID: 34",
|
||||
"currentJobId": 34,
|
||||
"lastRunAt": "2025-05-26T16:14:13.977Z"
|
||||
}
|
||||
]
|
||||
[]
|
||||
@@ -916,46 +916,37 @@ type BackupMutations {
|
||||
|
||||
input CreateBackupJobConfigInput {
|
||||
name: String!
|
||||
backupMode: BackupMode! = PREPROCESSING
|
||||
sourcePath: String!
|
||||
backupType: BackupType! = RAW
|
||||
remoteName: String!
|
||||
destinationPath: String!
|
||||
schedule: String!
|
||||
schedule: String
|
||||
enabled: Boolean! = true
|
||||
rcloneOptions: JSON
|
||||
|
||||
"""Preprocessing configuration for this backup job"""
|
||||
preprocessConfig: PreprocessConfigInput
|
||||
"""Backup configuration for this backup job"""
|
||||
backupConfig: BackupConfigInput
|
||||
}
|
||||
|
||||
"""
|
||||
The mode of backup to perform (Raw file backup or Preprocessing-based).
|
||||
Type of backup to perform (ZFS snapshot, Flash backup, Custom script, or Raw file backup)
|
||||
"""
|
||||
enum BackupMode {
|
||||
enum BackupType {
|
||||
ZFS
|
||||
FLASH
|
||||
SCRIPT
|
||||
RAW
|
||||
PREPROCESSING
|
||||
}
|
||||
|
||||
input PreprocessConfigInput {
|
||||
"""Type of preprocessing to perform"""
|
||||
type: PreprocessType!
|
||||
zfsConfig: ZfsPreprocessConfigInput
|
||||
flashConfig: FlashPreprocessConfigInput
|
||||
scriptConfig: ScriptPreprocessConfigInput
|
||||
|
||||
"""Timeout for preprocessing in seconds"""
|
||||
input BackupConfigInput {
|
||||
"""Timeout for backup operation in seconds"""
|
||||
timeout: Float! = 3600
|
||||
|
||||
"""Whether to cleanup on failure"""
|
||||
cleanupOnFailure: Boolean! = true
|
||||
}
|
||||
|
||||
"""Type of preprocessing to perform before backup"""
|
||||
enum PreprocessType {
|
||||
NONE
|
||||
ZFS
|
||||
FLASH
|
||||
SCRIPT
|
||||
zfsConfig: ZfsPreprocessConfigInput
|
||||
flashConfig: FlashPreprocessConfigInput
|
||||
scriptConfig: ScriptPreprocessConfigInput
|
||||
rawConfig: RawBackupConfigInput
|
||||
}
|
||||
|
||||
input ZfsPreprocessConfigInput {
|
||||
@@ -1003,17 +994,28 @@ input ScriptPreprocessConfigInput {
|
||||
outputPath: String!
|
||||
}
|
||||
|
||||
input RawBackupConfigInput {
|
||||
"""Source path to backup"""
|
||||
sourcePath: String!
|
||||
|
||||
"""File patterns to exclude from backup"""
|
||||
excludePatterns: [String!]
|
||||
|
||||
"""File patterns to include in backup"""
|
||||
includePatterns: [String!]
|
||||
}
|
||||
|
||||
input UpdateBackupJobConfigInput {
|
||||
name: String
|
||||
sourcePath: String
|
||||
backupType: BackupType
|
||||
remoteName: String
|
||||
destinationPath: String
|
||||
schedule: String
|
||||
enabled: Boolean
|
||||
rcloneOptions: JSON
|
||||
|
||||
"""Preprocessing configuration for this backup job"""
|
||||
preprocessConfig: PreprocessConfigInput
|
||||
"""Backup configuration for this backup job"""
|
||||
backupConfig: BackupConfigInput
|
||||
lastRunStatus: String
|
||||
currentJobId: String
|
||||
lastRunAt: String
|
||||
@@ -1173,13 +1175,19 @@ type ScriptPreprocessConfig {
|
||||
outputPath: String!
|
||||
}
|
||||
|
||||
type PreprocessConfig {
|
||||
type: PreprocessType!
|
||||
type RawBackupConfig {
|
||||
sourcePath: String!
|
||||
excludePatterns: [String!]
|
||||
includePatterns: [String!]
|
||||
}
|
||||
|
||||
type BackupConfig {
|
||||
timeout: Float!
|
||||
cleanupOnFailure: Boolean!
|
||||
zfsConfig: ZfsPreprocessConfig
|
||||
flashConfig: FlashPreprocessConfig
|
||||
scriptConfig: ScriptPreprocessConfig
|
||||
timeout: Float!
|
||||
cleanupOnFailure: Boolean!
|
||||
rawConfig: RawBackupConfig
|
||||
}
|
||||
|
||||
type RCloneDrive {
|
||||
@@ -1377,10 +1385,9 @@ type BackupJobConfig implements Node {
|
||||
|
||||
"""Human-readable name for this backup job"""
|
||||
name: String!
|
||||
backupMode: BackupMode!
|
||||
|
||||
"""Source path to backup"""
|
||||
sourcePath: String!
|
||||
"""Type of backup to perform"""
|
||||
backupType: BackupType!
|
||||
|
||||
"""Remote name from rclone config"""
|
||||
remoteName: String!
|
||||
@@ -1397,8 +1404,8 @@ type BackupJobConfig implements Node {
|
||||
"""RClone options (e.g., --transfers, --checkers)"""
|
||||
rcloneOptions: JSON
|
||||
|
||||
"""Preprocessing configuration for this backup job"""
|
||||
preprocessConfig: PreprocessConfig
|
||||
"""Backup configuration for this backup job"""
|
||||
backupConfig: BackupConfig
|
||||
|
||||
"""When this config was created"""
|
||||
createdAt: DateTime!
|
||||
|
||||
@@ -16,24 +16,15 @@ import {
|
||||
import { GraphQLJSON } from 'graphql-scalars';
|
||||
|
||||
import {
|
||||
PreprocessConfig,
|
||||
PreprocessConfigInput,
|
||||
BackupConfig,
|
||||
BackupConfigInput,
|
||||
BackupType,
|
||||
} from '@app/unraid-api/graph/resolvers/backup/preprocessing/preprocessing.types.js';
|
||||
import { Node } from '@app/unraid-api/graph/resolvers/base.model.js';
|
||||
import { RCloneJob } from '@app/unraid-api/graph/resolvers/rclone/rclone.model.js';
|
||||
import { PrefixedID } from '@app/unraid-api/graph/scalars/graphql-type-prefixed-id.js';
|
||||
import { DataSlice } from '@app/unraid-api/types/json-forms.js';
|
||||
|
||||
export enum BackupMode {
|
||||
RAW = 'RAW',
|
||||
PREPROCESSING = 'PREPROCESSING',
|
||||
}
|
||||
|
||||
registerEnumType(BackupMode, {
|
||||
name: 'BackupMode',
|
||||
description: 'The mode of backup to perform (Raw file backup or Preprocessing-based).',
|
||||
});
|
||||
|
||||
@ObjectType({
|
||||
implements: () => Node,
|
||||
})
|
||||
@@ -98,11 +89,8 @@ export class BackupJobConfig extends Node {
|
||||
@Field(() => String, { description: 'Human-readable name for this backup job' })
|
||||
name!: string;
|
||||
|
||||
@Field(() => BackupMode)
|
||||
backupMode!: BackupMode;
|
||||
|
||||
@Field(() => String, { description: 'Source path to backup' })
|
||||
sourcePath!: string;
|
||||
@Field(() => BackupType, { description: 'Type of backup to perform' })
|
||||
backupType!: BackupType;
|
||||
|
||||
@Field(() => String, { description: 'Remote name from rclone config' })
|
||||
remoteName!: string;
|
||||
@@ -124,11 +112,11 @@ export class BackupJobConfig extends Node {
|
||||
})
|
||||
rcloneOptions?: Record<string, unknown>;
|
||||
|
||||
@Field(() => PreprocessConfig, {
|
||||
description: 'Preprocessing configuration for this backup job',
|
||||
@Field(() => BackupConfig, {
|
||||
description: 'Backup configuration for this backup job',
|
||||
nullable: true,
|
||||
})
|
||||
preprocessConfig?: PreprocessConfig;
|
||||
backupConfig?: BackupConfig;
|
||||
|
||||
@Field(() => Date, { description: 'When this config was created' })
|
||||
createdAt!: Date;
|
||||
@@ -150,77 +138,17 @@ export class BackupJobConfig extends Node {
|
||||
}
|
||||
|
||||
@InputType()
|
||||
export class CreateBackupJobConfigInput {
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name!: string;
|
||||
|
||||
@Field(() => BackupMode, { defaultValue: BackupMode.PREPROCESSING })
|
||||
@IsEnum(BackupMode)
|
||||
@IsNotEmpty()
|
||||
backupMode?: BackupMode;
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
@ValidateIf((o) => o.backupMode === BackupMode.RAW)
|
||||
@IsNotEmpty({ message: 'sourcePath should not be empty when backupMode is RAW' })
|
||||
sourcePath!: string;
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
remoteName!: string;
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
destinationPath!: string;
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Matches(
|
||||
/^(\*|[0-5]?\d)(\s+(\*|[0-1]?\d|2[0-3]))(\s+(\*|[1-2]?\d|3[0-1]))(\s+(\*|[1-9]|1[0-2]))(\s+(\*|[0-6]))$/,
|
||||
{
|
||||
message: 'schedule must be a valid cron expression',
|
||||
}
|
||||
)
|
||||
schedule!: string;
|
||||
|
||||
@Field(() => Boolean, { defaultValue: true })
|
||||
@IsBoolean()
|
||||
enabled!: boolean;
|
||||
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
rcloneOptions?: Record<string, unknown>;
|
||||
|
||||
@Field(() => PreprocessConfigInput, {
|
||||
description: 'Preprocessing configuration for this backup job',
|
||||
nullable: true,
|
||||
})
|
||||
@IsOptional()
|
||||
@ValidateIf((o) => o.backupMode === BackupMode.PREPROCESSING)
|
||||
@ValidateNested()
|
||||
@Type(() => PreprocessConfigInput)
|
||||
preprocessConfig?: PreprocessConfigInput;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
export class UpdateBackupJobConfigInput {
|
||||
export class BaseBackupJobConfigInput {
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name?: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@Field(() => BackupType, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
sourcePath?: string;
|
||||
@IsEnum(BackupType)
|
||||
backupType?: BackupType;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsOptional()
|
||||
@@ -237,7 +165,7 @@ export class UpdateBackupJobConfigInput {
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@ValidateIf((o) => o.schedule && o.schedule.length > 0)
|
||||
@Matches(
|
||||
/^(\*|[0-5]?\d)(\s+(\*|[0-1]?\d|2[0-3]))(\s+(\*|[1-2]?\d|3[0-1]))(\s+(\*|[1-9]|1[0-2]))(\s+(\*|[0-6]))$/,
|
||||
{
|
||||
@@ -256,15 +184,46 @@ export class UpdateBackupJobConfigInput {
|
||||
@IsObject()
|
||||
rcloneOptions?: Record<string, unknown>;
|
||||
|
||||
@Field(() => PreprocessConfigInput, {
|
||||
description: 'Preprocessing configuration for this backup job',
|
||||
@Field(() => BackupConfigInput, {
|
||||
description: 'Backup configuration for this backup job',
|
||||
nullable: true,
|
||||
})
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => PreprocessConfigInput)
|
||||
preprocessConfig?: PreprocessConfigInput;
|
||||
@Type(() => BackupConfigInput)
|
||||
backupConfig?: BackupConfigInput;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
export class CreateBackupJobConfigInput extends BaseBackupJobConfigInput {
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
declare name: string;
|
||||
|
||||
@Field(() => BackupType, { defaultValue: BackupType.RAW })
|
||||
@IsEnum(BackupType)
|
||||
@IsNotEmpty()
|
||||
declare backupType: BackupType;
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
declare remoteName: string;
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
declare destinationPath: string;
|
||||
|
||||
@Field(() => Boolean, { defaultValue: true })
|
||||
@IsBoolean()
|
||||
@ValidateIf((o) => o.schedule && o.schedule.length > 0)
|
||||
declare enabled: boolean;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
export class UpdateBackupJobConfigInput extends BaseBackupJobConfigInput {
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@ import { Field, InputType, ObjectType, registerEnumType } from '@nestjs/graphql'
|
||||
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
@@ -14,16 +15,17 @@ import {
|
||||
} from 'class-validator';
|
||||
import { GraphQLJSON } from 'graphql-scalars';
|
||||
|
||||
export enum PreprocessType {
|
||||
NONE = 'none',
|
||||
ZFS = 'zfs',
|
||||
FLASH = 'flash',
|
||||
SCRIPT = 'script',
|
||||
export enum BackupType {
|
||||
ZFS = 'ZFS',
|
||||
FLASH = 'FLASH',
|
||||
SCRIPT = 'SCRIPT',
|
||||
RAW = 'RAW',
|
||||
}
|
||||
|
||||
registerEnumType(PreprocessType, {
|
||||
name: 'PreprocessType',
|
||||
description: 'Type of preprocessing to perform before backup',
|
||||
registerEnumType(BackupType, {
|
||||
name: 'BackupType',
|
||||
description:
|
||||
'Type of backup to perform (ZFS snapshot, Flash backup, Custom script, or Raw file backup)',
|
||||
});
|
||||
|
||||
@InputType()
|
||||
@@ -151,46 +153,80 @@ export class ScriptPreprocessConfig {
|
||||
}
|
||||
|
||||
@InputType()
|
||||
export class PreprocessConfigInput {
|
||||
@Field(() => PreprocessType, { description: 'Type of preprocessing to perform' })
|
||||
@IsEnum(PreprocessType)
|
||||
type!: PreprocessType;
|
||||
export class RawBackupConfigInput {
|
||||
@Field(() => String, { description: 'Source path to backup' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
sourcePath!: string;
|
||||
|
||||
@Field(() => [String], { description: 'File patterns to exclude from backup', nullable: true })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
excludePatterns?: string[];
|
||||
|
||||
@Field(() => [String], { description: 'File patterns to include in backup', nullable: true })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
includePatterns?: string[];
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class RawBackupConfig {
|
||||
@Field(() => String)
|
||||
sourcePath!: string;
|
||||
|
||||
@Field(() => [String], { nullable: true })
|
||||
excludePatterns?: string[];
|
||||
|
||||
@Field(() => [String], { nullable: true })
|
||||
includePatterns?: string[];
|
||||
}
|
||||
|
||||
@InputType()
|
||||
export class BackupConfigInput {
|
||||
@Field(() => Number, { description: 'Timeout for backup operation in seconds', defaultValue: 3600 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
timeout?: number;
|
||||
|
||||
@Field(() => Boolean, { description: 'Whether to cleanup on failure', defaultValue: true })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
cleanupOnFailure?: boolean;
|
||||
|
||||
@Field(() => ZfsPreprocessConfigInput, { nullable: true })
|
||||
@IsOptional()
|
||||
@ValidateIf((o) => o.type === PreprocessType.ZFS)
|
||||
@ValidateNested()
|
||||
@Type(() => ZfsPreprocessConfigInput)
|
||||
zfsConfig?: ZfsPreprocessConfigInput;
|
||||
|
||||
@Field(() => FlashPreprocessConfigInput, { nullable: true })
|
||||
@IsOptional()
|
||||
@ValidateIf((o) => o.type === PreprocessType.FLASH)
|
||||
@ValidateNested()
|
||||
@Type(() => FlashPreprocessConfigInput)
|
||||
flashConfig?: FlashPreprocessConfigInput;
|
||||
|
||||
@Field(() => ScriptPreprocessConfigInput, { nullable: true })
|
||||
@IsOptional()
|
||||
@ValidateIf((o) => o.type === PreprocessType.SCRIPT)
|
||||
@ValidateNested()
|
||||
@Type(() => ScriptPreprocessConfigInput)
|
||||
scriptConfig?: ScriptPreprocessConfigInput;
|
||||
|
||||
@Field(() => Number, { description: 'Timeout for preprocessing in seconds', defaultValue: 3600 })
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
timeout!: number;
|
||||
|
||||
@Field(() => Boolean, { description: 'Whether to cleanup on failure', defaultValue: true })
|
||||
@IsBoolean()
|
||||
cleanupOnFailure!: boolean;
|
||||
@Field(() => RawBackupConfigInput, { nullable: true })
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => RawBackupConfigInput)
|
||||
rawConfig?: RawBackupConfigInput;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class PreprocessConfig {
|
||||
@Field(() => PreprocessType)
|
||||
type!: PreprocessType;
|
||||
export class BackupConfig {
|
||||
@Field(() => Number)
|
||||
timeout!: number;
|
||||
|
||||
@Field(() => Boolean)
|
||||
cleanupOnFailure!: boolean;
|
||||
|
||||
@Field(() => ZfsPreprocessConfig, { nullable: true })
|
||||
zfsConfig?: ZfsPreprocessConfig;
|
||||
@@ -201,11 +237,8 @@ export class PreprocessConfig {
|
||||
@Field(() => ScriptPreprocessConfig, { nullable: true })
|
||||
scriptConfig?: ScriptPreprocessConfig;
|
||||
|
||||
@Field(() => Number)
|
||||
timeout!: number;
|
||||
|
||||
@Field(() => Boolean)
|
||||
cleanupOnFailure!: boolean;
|
||||
@Field(() => RawBackupConfig, { nullable: true })
|
||||
rawConfig?: RawBackupConfig;
|
||||
}
|
||||
|
||||
export interface PreprocessResult {
|
||||
@@ -222,8 +255,14 @@ export interface StreamingJobInfo {
|
||||
jobId: string;
|
||||
processId: number;
|
||||
startTime: Date;
|
||||
type: PreprocessType;
|
||||
type: BackupType;
|
||||
status: 'running' | 'completed' | 'failed' | 'cancelled';
|
||||
progress?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// Type aliases for backward compatibility
|
||||
export type PreprocessType = BackupType;
|
||||
export const PreprocessType = BackupType;
|
||||
export type PreprocessConfig = BackupConfig;
|
||||
export type PreprocessConfigInput = BackupConfigInput;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { RCloneJob, RCloneRemote } from '@app/unraid-api/graph/resolvers/rclone/
|
||||
* Types for rclone backup configuration UI
|
||||
*/
|
||||
export interface RcloneBackupConfigValues {
|
||||
configStep: number;
|
||||
configStep: { current: number; total: number };
|
||||
showAdvanced: boolean;
|
||||
name?: string;
|
||||
type?: string;
|
||||
@@ -77,7 +77,7 @@ export class RCloneService {
|
||||
*/
|
||||
async getCurrentSettings(): Promise<RcloneBackupConfigValues> {
|
||||
return {
|
||||
configStep: 0,
|
||||
configStep: { current: 0, total: 0 },
|
||||
showAdvanced: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
type UISchemaElement,
|
||||
} from '@jsonforms/core';
|
||||
import { DispatchRenderer, useJsonFormsLayout, type RendererProps } from '@jsonforms/vue';
|
||||
import { computed, inject, ref, type Ref } from 'vue';
|
||||
import { computed, inject, nextTick, onMounted, ref, type Ref } from 'vue';
|
||||
|
||||
// Define props based on RendererProps<Layout>
|
||||
const props = defineProps<RendererProps<Layout>>();
|
||||
@@ -48,15 +48,30 @@ const numSteps = computed(() => stepsConfig.value.length);
|
||||
// --- Current Step Logic --- Use injected core.data
|
||||
const currentStep = computed(() => {
|
||||
const stepData = core!.data?.configStep;
|
||||
// Handle both the new object format and the old number format
|
||||
|
||||
// Return current step if properly initialized
|
||||
if (typeof stepData === 'object' && stepData !== null && typeof stepData.current === 'number') {
|
||||
// Ensure step is within bounds
|
||||
return Math.max(0, Math.min(stepData.current, numSteps.value - 1));
|
||||
}
|
||||
// Fallback for initial state or old number format
|
||||
const numericStep = typeof stepData === 'number' ? stepData : 0;
|
||||
return Math.max(0, Math.min(numericStep, numSteps.value - 1));
|
||||
|
||||
// Return 0 as default if not initialized yet
|
||||
return 0;
|
||||
});
|
||||
|
||||
// Initialize configStep on mount
|
||||
onMounted(async () => {
|
||||
// Wait for next tick to ensure form data is available
|
||||
await nextTick();
|
||||
|
||||
const stepData = core!.data?.configStep;
|
||||
|
||||
// Only initialize if configStep doesn't exist or is in wrong format
|
||||
if (!stepData || typeof stepData !== 'object' || typeof stepData.current !== 'number') {
|
||||
const initialStep = { current: 0, total: numSteps.value };
|
||||
dispatch(Actions.update('configStep', () => initialStep));
|
||||
}
|
||||
});
|
||||
|
||||
const isLastStep = computed(() => numSteps.value > 0 && currentStep.value === numSteps.value - 1);
|
||||
|
||||
// --- Step Update Logic ---
|
||||
@@ -81,6 +96,7 @@ const currentStepElements = computed(() => {
|
||||
element.options.step === currentStep.value
|
||||
);
|
||||
});
|
||||
|
||||
return filtered;
|
||||
});
|
||||
|
||||
@@ -142,9 +158,6 @@ const getStepState = (stepIndex: number): StepState => {
|
||||
/>
|
||||
</StepperItem>
|
||||
</Stepper>
|
||||
|
||||
<!-- Render elements for the current step -->
|
||||
<!-- Added key to force re-render on step change, ensuring correct elements display -->
|
||||
<div class="current-step-content rounded-md border p-4 shadow" :key="`step-content-${currentStep}`">
|
||||
<DispatchRenderer
|
||||
v-for="(element, index) in currentStepElements"
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, provide, ref, watch } from 'vue';
|
||||
import { useMutation, useQuery } from '@vue/apollo-composable';
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import { Button, JsonForms } from '@unraid/ui';
|
||||
|
||||
import { CREATE_BACKUP_JOB_CONFIG_MUTATION, BACKUP_JOB_CONFIG_FORM_QUERY } from './backup-jobs.query';
|
||||
import type { CreateBackupJobConfigInput, InputMaybe, PreprocessConfigInput, BackupMode } from '~/composables/gql/graphql';
|
||||
import type { CreateBackupJobConfigInput } from '~/composables/gql/graphql';
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import { BACKUP_JOB_CONFIG_FORM_QUERY, CREATE_BACKUP_JOB_CONFIG_MUTATION } from './backup-jobs.query';
|
||||
|
||||
// Define emit events
|
||||
const emit = defineEmits<{
|
||||
complete: []
|
||||
cancel: []
|
||||
}>()
|
||||
complete: [];
|
||||
cancel: [];
|
||||
}>();
|
||||
|
||||
// Define types for form state
|
||||
interface ConfigStep {
|
||||
@@ -30,7 +31,8 @@ const {
|
||||
refetch: updateFormSchema,
|
||||
} = useQuery(BACKUP_JOB_CONFIG_FORM_QUERY, {
|
||||
input: {
|
||||
showAdvanced: typeof formState.value?.showAdvanced === 'boolean' ? formState.value.showAdvanced : false,
|
||||
showAdvanced:
|
||||
typeof formState.value?.showAdvanced === 'boolean' ? formState.value.showAdvanced : false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -39,21 +41,31 @@ let refetchTimeout: NodeJS.Timeout | null = null;
|
||||
watch(
|
||||
formState,
|
||||
async (newValue, oldValue) => {
|
||||
const newStepCurrent = typeof (newValue?.configStep) === 'object' ? (newValue.configStep as ConfigStep).current : (newValue?.configStep as number);
|
||||
const oldStepCurrent = typeof (oldValue?.configStep) === 'object' ? (oldValue.configStep as ConfigStep).current : (oldValue?.configStep as number);
|
||||
const newStepCurrent = (newValue?.configStep as ConfigStep)?.current ?? 0;
|
||||
const oldStepCurrent = (oldValue?.configStep as ConfigStep)?.current ?? 0;
|
||||
const newShowAdvanced = typeof newValue?.showAdvanced === 'boolean' ? newValue.showAdvanced : false;
|
||||
const oldShowAdvanced = typeof oldValue?.showAdvanced === 'boolean' ? oldValue.showAdvanced : false;
|
||||
const shouldRefetch = newShowAdvanced !== oldShowAdvanced || newStepCurrent !== oldStepCurrent;
|
||||
|
||||
if (shouldRefetch) {
|
||||
if (newShowAdvanced !== oldShowAdvanced) {
|
||||
console.log('[BackupJobConfigForm] showAdvanced changed:', newShowAdvanced);
|
||||
}
|
||||
if (newStepCurrent !== oldStepCurrent) {
|
||||
console.log('[BackupJobConfigForm] configStep.current changed:', newStepCurrent, 'from:', oldStepCurrent, 'Refetching schema.');
|
||||
console.log(
|
||||
'[BackupJobConfigForm] configStep.current changed:',
|
||||
newStepCurrent,
|
||||
'from:',
|
||||
oldStepCurrent,
|
||||
'Refetching schema.'
|
||||
);
|
||||
}
|
||||
|
||||
// Debounce refetch to prevent multiple rapid calls
|
||||
if (refetchTimeout) {
|
||||
clearTimeout(refetchTimeout);
|
||||
}
|
||||
|
||||
refetchTimeout = setTimeout(async () => {
|
||||
await updateFormSchema({
|
||||
input: {
|
||||
@@ -80,22 +92,9 @@ const {
|
||||
// Handle form submission
|
||||
const submitForm = async () => {
|
||||
try {
|
||||
const value = formState.value as Record<string, unknown>;
|
||||
console.log('value', value);
|
||||
console.log('[BackupJobConfigForm] submitForm', value);
|
||||
const input: CreateBackupJobConfigInput = {
|
||||
name: value?.name as string,
|
||||
destinationPath: value?.destinationPath as string,
|
||||
schedule: (value?.schedule as string) || '',
|
||||
enabled: value?.enabled as boolean,
|
||||
remoteName: value?.remoteName as string,
|
||||
sourcePath: (value?.sourcePath as string) || '',
|
||||
rcloneOptions: value?.rcloneOptions as Record<string, unknown>,
|
||||
preprocessConfig: value?.preprocessConfig as InputMaybe<PreprocessConfigInput> | undefined,
|
||||
backupMode: (value?.backupMode as BackupMode) || 'RAW' as BackupMode,
|
||||
};
|
||||
const { configStep, ...input } = formState.value;
|
||||
await createBackupJobConfig({
|
||||
input,
|
||||
input: input as CreateBackupJobConfigInput,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating backup job config:', error);
|
||||
@@ -116,14 +115,30 @@ onCreateDone(async ({ data }) => {
|
||||
|
||||
const parsedOriginalErrorMessage = computed(() => {
|
||||
const originalError = createError.value?.graphQLErrors?.[0]?.extensions?.originalError;
|
||||
if (originalError && typeof originalError === 'object' && originalError !== null && 'message' in originalError) {
|
||||
if (
|
||||
originalError &&
|
||||
typeof originalError === 'object' &&
|
||||
originalError !== null &&
|
||||
'message' in originalError
|
||||
) {
|
||||
return (originalError as { message: string | string[] }).message;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
let changeTimeout: NodeJS.Timeout | null = null;
|
||||
const onChange = ({ data }: { data: unknown }) => {
|
||||
console.log('[BackupJobConfigForm] onChange', data);
|
||||
// Clear any pending timeout
|
||||
if (changeTimeout) {
|
||||
clearTimeout(changeTimeout);
|
||||
}
|
||||
|
||||
// Debounce logging to reduce console spam
|
||||
changeTimeout = setTimeout(() => {
|
||||
console.log('[BackupJobConfigForm] onChange', data);
|
||||
changeTimeout = null;
|
||||
}, 300);
|
||||
|
||||
formState.value = data as Record<string, unknown>;
|
||||
};
|
||||
|
||||
@@ -132,8 +147,8 @@ const uiSchema = computed(() => formResult.value?.backupJobConfigForm?.uiSchema)
|
||||
|
||||
// Handle both number and object formats of configStep
|
||||
const getCurrentStep = computed(() => {
|
||||
const step = formState.value?.configStep;
|
||||
return typeof step === 'object' ? (step as ConfigStep).current : step as number;
|
||||
const step = formState.value?.configStep as ConfigStep;
|
||||
return step?.current ?? 0;
|
||||
});
|
||||
|
||||
// Get total steps from UI schema
|
||||
@@ -157,21 +172,33 @@ provide('isSubmitting', isCreating);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm">
|
||||
<div
|
||||
class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm"
|
||||
>
|
||||
<div class="p-6">
|
||||
<h2 class="text-xl font-medium mb-4 text-gray-900 dark:text-white">Configure Backup Job</h2>
|
||||
|
||||
<div v-if="createError" class="mb-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-300 rounded-md">
|
||||
<div
|
||||
v-if="createError"
|
||||
class="mb-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-300 rounded-md"
|
||||
>
|
||||
<p>{{ createError.message }}</p>
|
||||
<ul v-if="Array.isArray(parsedOriginalErrorMessage)" class="list-disc list-inside mt-2">
|
||||
<li v-for="(msg, index) in parsedOriginalErrorMessage" :key="index">{{ msg }}</li>
|
||||
</ul>
|
||||
<p v-else-if="typeof parsedOriginalErrorMessage === 'string' && parsedOriginalErrorMessage.length > 0" class="mt-2">
|
||||
<p
|
||||
v-else-if="
|
||||
typeof parsedOriginalErrorMessage === 'string' && parsedOriginalErrorMessage.length > 0
|
||||
"
|
||||
class="mt-2"
|
||||
>
|
||||
{{ parsedOriginalErrorMessage }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="formLoading" class="py-8 text-center text-gray-500 dark:text-gray-400">Loading configuration form...</div>
|
||||
<div v-if="formLoading" class="py-8 text-center text-gray-500 dark:text-gray-400">
|
||||
Loading configuration form...
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<div v-else-if="formResult?.backupJobConfigForm" class="mt-6 [&_.vertical-layout]:space-y-6">
|
||||
@@ -190,32 +217,23 @@ provide('isSubmitting', isCreating);
|
||||
v-if="!formLoading && uiSchema && isLastStep"
|
||||
class="mt-6 flex justify-end space-x-3 border-t border-gray-200 dark:border-gray-700 pt-6"
|
||||
>
|
||||
<Button variant="outline" @click="emit('cancel')">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button :loading="isCreating" @click="submitForm">
|
||||
Create Backup Job
|
||||
</Button>
|
||||
<Button variant="outline" @click="emit('cancel')"> Cancel </Button>
|
||||
<Button :loading="isCreating" @click="submitForm"> Create Backup Job </Button>
|
||||
</div>
|
||||
|
||||
<!-- If there's no stepped layout, show buttons at the bottom -->
|
||||
<div
|
||||
v-if="!formLoading && (!uiSchema || (numSteps === 0))"
|
||||
v-if="!formLoading && (!uiSchema || numSteps === 0)"
|
||||
class="mt-6 flex justify-end space-x-3 border-t border-gray-200 dark:border-gray-700 pt-6"
|
||||
>
|
||||
<Button variant="outline" @click="emit('cancel')">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button :loading="isCreating" @click="submitForm">
|
||||
Create Backup Job
|
||||
</Button>
|
||||
<Button variant="outline" @click="emit('cancel')"> Cancel </Button>
|
||||
<Button :loading="isCreating" @click="submitForm"> Create Backup Job </Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<style lang="postcss">
|
||||
/* Import unraid-ui globals first */
|
||||
@import '@unraid/ui/styles';
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -30,7 +30,7 @@ interface ConfigStep {
|
||||
|
||||
// Form state
|
||||
const formState = ref(props.initialState || {
|
||||
configStep: 0 as number | ConfigStep,
|
||||
configStep: { current: 0, total: 0 },
|
||||
showAdvanced: false,
|
||||
name: '',
|
||||
type: '',
|
||||
@@ -56,13 +56,8 @@ watch(
|
||||
formState,
|
||||
async (newValue, oldValue) => {
|
||||
// Get current step as number for comparison
|
||||
const newStep = typeof newValue.configStep === 'object'
|
||||
? (newValue.configStep as ConfigStep).current
|
||||
: newValue.configStep as number;
|
||||
|
||||
const oldStep = typeof oldValue.configStep === 'object'
|
||||
? (oldValue.configStep as ConfigStep).current
|
||||
: oldValue.configStep as number;
|
||||
const newStep = (newValue.configStep as ConfigStep)?.current ?? 0;
|
||||
const oldStep = (oldValue.configStep as ConfigStep)?.current ?? 0;
|
||||
|
||||
// Check if we need to refetch
|
||||
const shouldRefetch =
|
||||
@@ -138,7 +133,7 @@ onCreateDone(async ({ data }) => {
|
||||
|
||||
// Reset form and emit complete event
|
||||
formState.value = {
|
||||
configStep: 0,
|
||||
configStep: { current: 0, total: 0 },
|
||||
showAdvanced: false,
|
||||
name: '',
|
||||
type: '',
|
||||
@@ -179,8 +174,8 @@ const uiSchema = computed(() => formResult.value?.rclone?.configForm?.uiSchema);
|
||||
|
||||
// Handle both number and object formats of configStep
|
||||
const getCurrentStep = computed(() => {
|
||||
const step = formState.value.configStep;
|
||||
return typeof step === 'object' ? (step as ConfigStep).current : step as number;
|
||||
const step = formState.value.configStep as ConfigStep;
|
||||
return step?.current ?? 0;
|
||||
});
|
||||
|
||||
// Get total steps from UI schema
|
||||
|
||||
@@ -10,7 +10,7 @@ import RCloneConfig from './RCloneConfig.vue';
|
||||
import RemoteItem from './RemoteItem.vue';
|
||||
|
||||
interface FormState {
|
||||
configStep: number;
|
||||
configStep: { current: number; total: number };
|
||||
showAdvanced: boolean;
|
||||
name: string;
|
||||
type: string;
|
||||
@@ -63,7 +63,7 @@ const openCryptModal = (remote: { name: string, type: string }) => {
|
||||
const entropy = Math.random().toString(36).substring(2, 8);
|
||||
selectedRemote.value = remote;
|
||||
initialFormState.value = {
|
||||
configStep: 0,
|
||||
configStep: { current: 0, total: 0 },
|
||||
showAdvanced: false,
|
||||
name: `${remote.name}-crypt-${entropy}`,
|
||||
type: 'crypt',
|
||||
|
||||
Reference in New Issue
Block a user