From 69359902cbac75a7613eafe790f3b9515bf5f482 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Sat, 24 May 2025 07:42:39 -0400 Subject: [PATCH] chore: begin setting up new views and mutations for flash backup --- api/dev/Unraid.net/myservers.cfg | 2 +- api/generated-schema.graphql | 246 +++++++++---- .../resolvers/backup/backup-config.service.ts | 211 +++++++++++ .../graph/resolvers/backup/backup.model.ts | 242 ++++++++++++ .../graph/resolvers/backup/backup.module.ts | 14 + .../graph/resolvers/backup/backup.resolver.ts | 201 ++++++++++ .../graph/resolvers/backup/format.service.ts | 20 + .../jsonforms/backup-jsonforms-config.ts | 346 ++++++++++++++++++ .../flash-backup/flash-backup.model.ts | 53 --- .../flash-backup/flash-backup.module.ts | 11 - .../flash-backup/flash-backup.resolver.ts | 24 -- .../graph/resolvers/resolvers.module.ts | 4 +- web/components/Backup/BackupJobConfig.vue | 183 +++++++++ web/components/Backup/BackupJobConfigForm.vue | 240 ++++++++++++ web/components/Backup/BackupOverview.vue | 170 +++++++++ web/components/Backup/backup-jobs.query.ts | 75 ++++ web/composables/gql/gql.ts | 30 ++ web/composables/gql/graphql.ts | 194 +++++++++- web/pages/flashbackup.vue | 4 +- 19 files changed, 2100 insertions(+), 170 deletions(-) create mode 100644 api/src/unraid-api/graph/resolvers/backup/backup-config.service.ts create mode 100644 api/src/unraid-api/graph/resolvers/backup/backup.model.ts create mode 100644 api/src/unraid-api/graph/resolvers/backup/backup.module.ts create mode 100644 api/src/unraid-api/graph/resolvers/backup/backup.resolver.ts create mode 100644 api/src/unraid-api/graph/resolvers/backup/format.service.ts create mode 100644 api/src/unraid-api/graph/resolvers/backup/jsonforms/backup-jsonforms-config.ts delete mode 100644 api/src/unraid-api/graph/resolvers/flash-backup/flash-backup.model.ts delete mode 100644 api/src/unraid-api/graph/resolvers/flash-backup/flash-backup.module.ts delete mode 100644 api/src/unraid-api/graph/resolvers/flash-backup/flash-backup.resolver.ts create mode 100644 web/components/Backup/BackupJobConfig.vue create mode 100644 web/components/Backup/BackupJobConfigForm.vue create mode 100644 web/components/Backup/BackupOverview.vue create mode 100644 web/components/Backup/backup-jobs.query.ts diff --git a/api/dev/Unraid.net/myservers.cfg b/api/dev/Unraid.net/myservers.cfg index 1c664dad2..1f8d0bb03 100644 --- a/api/dev/Unraid.net/myservers.cfg +++ b/api/dev/Unraid.net/myservers.cfg @@ -1,5 +1,5 @@ [api] -version="4.4.1" +version="4.8.0" extraOrigins="https://google.com,https://test.com" [local] sandbox="yes" diff --git a/api/generated-schema.graphql b/api/generated-schema.graphql index 986b0fef0..bca01c37b 100644 --- a/api/generated-schema.graphql +++ b/api/generated-schema.graphql @@ -999,6 +999,124 @@ A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date """ scalar DateTime +type RCloneDrive { + """Provider name""" + name: String! + + """Provider options and configuration schema""" + options: JSON! +} + +type RCloneBackupConfigForm { + id: ID! + dataSchema: JSON! + uiSchema: JSON! +} + +type RCloneBackupSettings { + configForm(formOptions: RCloneConfigFormInput): RCloneBackupConfigForm! + drives: [RCloneDrive!]! + remotes: [RCloneRemote!]! +} + +input RCloneConfigFormInput { + providerType: String + showAdvanced: Boolean = false + parameters: JSON +} + +type RCloneRemote { + name: String! + type: String! + parameters: JSON! + + """Complete remote configuration""" + config: JSON! +} + +type Backup implements Node { + id: PrefixedID! + jobs: [BackupJob!]! + configs: [BackupJobConfig!]! + + """Get the status for the backup service""" + status: BackupStatus! +} + +type BackupStatus { + """Status message indicating the outcome of the backup initiation.""" + status: String! + + """Job ID if available, can be used to check job status.""" + jobId: String +} + +type BackupJob { + """Job ID""" + id: String! + + """Job type (e.g., sync/copy)""" + type: String! + + """Job status and statistics""" + stats: JSON! + + """Formatted bytes transferred""" + formattedBytes: String + + """Formatted transfer speed""" + formattedSpeed: String + + """Formatted elapsed time""" + formattedElapsedTime: String + + """Formatted ETA""" + formattedEta: String +} + +type BackupJobConfig { + id: PrefixedID! + + """Human-readable name for this backup job""" + name: String! + + """Source path to backup""" + sourcePath: String! + + """Remote name from rclone config""" + remoteName: String! + + """Destination path on the remote""" + destinationPath: String! + + """Cron schedule expression (e.g., "0 2 * * *" for daily at 2AM)""" + schedule: String! + + """Whether this backup job is enabled""" + enabled: Boolean! + + """RClone options (e.g., --transfers, --checkers)""" + rcloneOptions: JSON + + """When this config was created""" + createdAt: DateTime! + + """When this config was last updated""" + updatedAt: DateTime! + + """Last time this job ran""" + lastRunAt: DateTime + + """Status of last run""" + lastRunStatus: String +} + +type BackupJobConfigForm { + id: ID! + dataSchema: JSON! + uiSchema: JSON! +} + type Config implements Node { id: PrefixedID! valid: Boolean @@ -1356,49 +1474,6 @@ type Docker implements Node { networks(skipCache: Boolean! = false): [DockerNetwork!]! } -type FlashBackupStatus { - """Status message indicating the outcome of the backup initiation.""" - status: String! - - """Job ID if available, can be used to check job status.""" - jobId: String -} - -type RCloneDrive { - """Provider name""" - name: String! - - """Provider options and configuration schema""" - options: JSON! -} - -type RCloneBackupConfigForm { - id: ID! - dataSchema: JSON! - uiSchema: JSON! -} - -type RCloneBackupSettings { - configForm(formOptions: RCloneConfigFormInput): RCloneBackupConfigForm! - drives: [RCloneDrive!]! - remotes: [RCloneRemote!]! -} - -input RCloneConfigFormInput { - providerType: String - showAdvanced: Boolean = false - parameters: JSON -} - -type RCloneRemote { - name: String! - type: String! - parameters: JSON! - - """Complete remote configuration""" - config: JSON! -} - type Flash implements Node { id: PrefixedID! guid: String! @@ -1591,6 +1666,19 @@ type Query { """All possible permissions for API keys""" apiKeyPossiblePermissions: [Permission!]! + + """Get backup service information""" + backup: Backup! + + """Get a specific backup job configuration""" + backupJobConfig(id: String!): BackupJobConfig + + """Get status of a specific backup job""" + backupJob(jobId: String!): BackupJob + + """Get the JSON schema for backup job configuration form""" + backupJobConfigForm(input: BackupJobConfigFormInput): BackupJobConfigForm! + rclone: RCloneBackupSettings! connect: Connect! remoteAccess: RemoteAccess! extraAllowedOrigins: [String!]! @@ -1600,11 +1688,14 @@ type Query { docker: Docker! disks: [Disk!]! disk(id: PrefixedID!): Disk! - rclone: RCloneBackupSettings! health: String! getDemo: String! } +input BackupJobConfigFormInput { + showAdvanced: Boolean! = false +} + type Mutation { """Creates a new notification record""" createNotification(input: NotificationData!): Notification! @@ -1631,15 +1722,24 @@ type Mutation { parityCheck: ParityCheckMutations! apiKey: ApiKeyMutations! rclone: RCloneMutations! + + """Create a new backup job configuration""" + createBackupJobConfig(input: CreateBackupJobConfigInput!): BackupJobConfig! + + """Update a backup job configuration""" + updateBackupJobConfig(id: String!, input: UpdateBackupJobConfigInput!): BackupJobConfig + + """Delete a backup job configuration""" + deleteBackupJobConfig(id: String!): Boolean! + + """Initiates a backup using a configured remote.""" + initiateBackup(input: InitiateBackupInput!): BackupStatus! updateApiSettings(input: ApiSettingsInput!): ConnectSettingsValues! connectSignIn(input: ConnectSignInInput!): Boolean! connectSignOut: Boolean! setupRemoteAccess(input: SetupRemoteAccessInput!): Boolean! setAdditionalAllowedOrigins(input: AllowedOriginInput!): [String!]! enableDynamicRemoteAccess(input: EnableDynamicRemoteAccessInput!): Boolean! - - """Initiates a flash drive backup using a configured remote.""" - initiateFlashBackup(input: InitiateFlashBackupInput!): FlashBackupStatus! setDemo: String! } @@ -1651,6 +1751,42 @@ input NotificationData { link: String } +input CreateBackupJobConfigInput { + name: String! + sourcePath: String! + remoteName: String! + destinationPath: String! + schedule: String! + enabled: Boolean! = true + rcloneOptions: JSON +} + +input UpdateBackupJobConfigInput { + name: String + sourcePath: String + remoteName: String + destinationPath: String + schedule: String + enabled: Boolean + rcloneOptions: JSON +} + +input InitiateBackupInput { + """The name of the remote configuration to use for the backup.""" + remoteName: String! + + """Source path to backup.""" + sourcePath: String! + + """Destination path on the remote.""" + destinationPath: String! + + """ + Additional options for the backup operation, such as --dry-run or --transfers. + """ + options: JSON +} + input ApiSettingsInput { """ If true, the GraphQL sandbox will be enabled and available at /graphql. If false, the GraphQL sandbox will be disabled and only the production API will be available. @@ -1736,22 +1872,6 @@ input AccessUrlInput { ipv6: URL } -input InitiateFlashBackupInput { - """The name of the remote configuration to use for the backup.""" - remoteName: String! - - """Source path to backup (typically the flash drive).""" - sourcePath: String! - - """Destination path on the remote.""" - destinationPath: String! - - """ - Additional options for the backup operation, such as --dry-run or --transfers. - """ - options: JSON -} - type Subscription { displaySubscription: Display! infoSubscription: Info! diff --git a/api/src/unraid-api/graph/resolvers/backup/backup-config.service.ts b/api/src/unraid-api/graph/resolvers/backup/backup-config.service.ts new file mode 100644 index 000000000..67859b133 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/backup/backup-config.service.ts @@ -0,0 +1,211 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { SchedulerRegistry } from '@nestjs/schedule'; +import { existsSync } from 'fs'; +import { readFile, writeFile } from 'fs/promises'; + +import { CronJob } from 'cron'; +import { v4 as uuidv4 } from 'uuid'; + +import { + BackupJobConfig, + CreateBackupJobConfigInput, + UpdateBackupJobConfigInput, +} from '@app/unraid-api/graph/resolvers/backup/backup.model.js'; +import { RCloneService } from '@app/unraid-api/graph/resolvers/rclone/rclone.service.js'; + +interface BackupJobConfigData { + id: string; + name: string; + sourcePath: string; + remoteName: string; + destinationPath: string; + schedule: string; + enabled: boolean; + rcloneOptions?: Record; + createdAt: string; + updatedAt: string; + lastRunAt?: string; + lastRunStatus?: string; +} + +@Injectable() +export class BackupConfigService { + private readonly logger = new Logger(BackupConfigService.name); + private readonly configPath = '/boot/config/backup-jobs.json'; + private configs: Map = new Map(); + + constructor( + private readonly rcloneService: RCloneService, + private readonly schedulerRegistry: SchedulerRegistry + ) { + this.loadConfigs(); + } + + async createBackupJobConfig(input: CreateBackupJobConfigInput): Promise { + const id = uuidv4(); + const now = new Date().toISOString(); + + const config: BackupJobConfigData = { + id, + ...input, + createdAt: now, + updatedAt: now, + }; + + this.configs.set(id, config); + await this.saveConfigs(); + + if (config.enabled) { + this.scheduleJob(config); + } + + return this.mapToGraphQL(config); + } + + async updateBackupJobConfig( + id: string, + input: UpdateBackupJobConfigInput + ): Promise { + const existing = this.configs.get(id); + if (!existing) return null; + + const updated: BackupJobConfigData = { + ...existing, + ...input, + updatedAt: new Date().toISOString(), + }; + + this.configs.set(id, updated); + await this.saveConfigs(); + + this.unscheduleJob(id); + if (updated.enabled) { + this.scheduleJob(updated); + } + + return this.mapToGraphQL(updated); + } + + async deleteBackupJobConfig(id: string): Promise { + const config = this.configs.get(id); + if (!config) return false; + + this.unscheduleJob(id); + this.configs.delete(id); + await this.saveConfigs(); + return true; + } + + async getBackupJobConfig(id: string): Promise { + const config = this.configs.get(id); + return config ? this.mapToGraphQL(config) : null; + } + + async getAllBackupJobConfigs(): Promise { + return Array.from(this.configs.values()).map((config) => this.mapToGraphQL(config)); + } + + private async executeBackupJob(config: BackupJobConfigData): Promise { + this.logger.log(`Executing backup job: ${config.name}`); + + try { + const result = await this.rcloneService['rcloneApiService'].startBackup({ + srcPath: config.sourcePath, + dstPath: `${config.remoteName}:${config.destinationPath}`, + options: config.rcloneOptions, + }); + + config.lastRunAt = new Date().toISOString(); + config.lastRunStatus = `Started with job ID: ${result.jobId}`; + this.configs.set(config.id, config); + await this.saveConfigs(); + + this.logger.log(`Backup job ${config.name} started successfully: ${result.jobId}`); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + config.lastRunAt = new Date().toISOString(); + config.lastRunStatus = `Failed: ${errorMessage}`; + this.configs.set(config.id, config); + await this.saveConfigs(); + + this.logger.error(`Backup job ${config.name} failed:`, error); + } + } + + private scheduleJob(config: BackupJobConfigData): void { + try { + const job = new CronJob( + config.schedule, + () => this.executeBackupJob(config), + null, + false, + 'UTC' + ); + + this.schedulerRegistry.addCronJob(`backup-${config.id}`, job); + job.start(); + this.logger.log(`Scheduled backup job: ${config.name} with schedule: ${config.schedule}`); + } catch (error) { + this.logger.error(`Failed to schedule backup job ${config.name}:`, error); + } + } + + private unscheduleJob(id: string): void { + try { + const jobName = `backup-${id}`; + if (this.schedulerRegistry.doesExist('cron', jobName)) { + this.schedulerRegistry.deleteCronJob(jobName); + this.logger.log(`Unscheduled backup job: ${id}`); + } + } catch (error) { + this.logger.error(`Failed to unschedule backup job ${id}:`, error); + } + } + + private async loadConfigs(): Promise { + try { + if (existsSync(this.configPath)) { + const data = await readFile(this.configPath, 'utf-8'); + const configs: BackupJobConfigData[] = JSON.parse(data); + + this.configs.clear(); + configs.forEach((config) => { + this.configs.set(config.id, config); + if (config.enabled) { + this.scheduleJob(config); + } + }); + + this.logger.log(`Loaded ${configs.length} backup job configurations`); + } + } catch (error) { + this.logger.error('Failed to load backup configurations:', error); + } + } + + private async saveConfigs(): Promise { + try { + const configs = Array.from(this.configs.values()); + await writeFile(this.configPath, JSON.stringify(configs, null, 2)); + } catch (error) { + this.logger.error('Failed to save backup configurations:', error); + } + } + + private mapToGraphQL(config: BackupJobConfigData): BackupJobConfig { + return { + id: config.id, + name: config.name, + sourcePath: config.sourcePath, + remoteName: config.remoteName, + destinationPath: config.destinationPath, + schedule: config.schedule, + enabled: config.enabled, + rcloneOptions: config.rcloneOptions, + createdAt: new Date(config.createdAt), + updatedAt: new Date(config.updatedAt), + lastRunAt: config.lastRunAt ? new Date(config.lastRunAt) : undefined, + lastRunStatus: config.lastRunStatus, + }; + } +} diff --git a/api/src/unraid-api/graph/resolvers/backup/backup.model.ts b/api/src/unraid-api/graph/resolvers/backup/backup.model.ts new file mode 100644 index 000000000..baed83aed --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/backup/backup.model.ts @@ -0,0 +1,242 @@ +import { Field, ID, InputType, ObjectType } from '@nestjs/graphql'; + +import { type Layout } from '@jsonforms/core'; +import { IsBoolean, IsNotEmpty, IsObject, IsOptional, IsString, Matches } from 'class-validator'; +import { GraphQLJSON } from 'graphql-scalars'; + +import { Node } from '@app/unraid-api/graph/resolvers/base.model.js'; +import { DataSlice } from '@app/unraid-api/types/json-forms.js'; + +@ObjectType({ + implements: () => Node, +}) +export class Backup extends Node { + @Field(() => [BackupJob]) + jobs!: BackupJob[]; + + @Field(() => [BackupJobConfig]) + configs!: BackupJobConfig[]; +} + +@InputType() +export class InitiateBackupInput { + @Field(() => String, { description: 'The name of the remote configuration to use for the backup.' }) + @IsString() + @IsNotEmpty() + remoteName!: string; + + @Field(() => String, { description: 'Source path to backup.' }) + @IsString() + @IsNotEmpty() + sourcePath!: string; + + @Field(() => String, { description: 'Destination path on the remote.' }) + @IsString() + @IsNotEmpty() + destinationPath!: string; + + @Field(() => GraphQLJSON, { + description: 'Additional options for the backup operation, such as --dry-run or --transfers.', + nullable: true, + }) + @IsOptional() + @IsObject() + options?: Record; +} + +@ObjectType() +export class BackupStatus { + @Field(() => String, { + description: 'Status message indicating the outcome of the backup initiation.', + }) + status!: string; + + @Field(() => String, { + description: 'Job ID if available, can be used to check job status.', + nullable: true, + }) + jobId?: string; +} + +@ObjectType() +export class BackupJob { + @Field(() => String, { description: 'Job ID' }) + id!: string; + + @Field(() => String, { description: 'Job type (e.g., sync/copy)' }) + type!: string; + + @Field(() => GraphQLJSON, { description: 'Job status and statistics' }) + stats!: Record; + + @Field(() => String, { description: 'Formatted bytes transferred', nullable: true }) + formattedBytes?: string; + + @Field(() => String, { description: 'Formatted transfer speed', nullable: true }) + formattedSpeed?: string; + + @Field(() => String, { description: 'Formatted elapsed time', nullable: true }) + formattedElapsedTime?: string; + + @Field(() => String, { description: 'Formatted ETA', nullable: true }) + formattedEta?: string; +} + +@ObjectType() +export class RCloneWebGuiInfo { + @Field() + url!: string; +} + +@ObjectType() +export class BackupJobConfig extends Node { + @Field(() => String, { description: 'Human-readable name for this backup job' }) + name!: string; + + @Field(() => String, { description: 'Source path to backup' }) + sourcePath!: string; + + @Field(() => String, { description: 'Remote name from rclone config' }) + remoteName!: string; + + @Field(() => String, { description: 'Destination path on the remote' }) + destinationPath!: string; + + @Field(() => String, { + description: 'Cron schedule expression (e.g., "0 2 * * *" for daily at 2AM)', + }) + schedule!: string; + + @Field(() => Boolean, { description: 'Whether this backup job is enabled' }) + enabled!: boolean; + + @Field(() => GraphQLJSON, { + description: 'RClone options (e.g., --transfers, --checkers)', + nullable: true, + }) + rcloneOptions?: Record; + + @Field(() => Date, { description: 'When this config was created' }) + createdAt!: Date; + + @Field(() => Date, { description: 'When this config was last updated' }) + updatedAt!: Date; + + @Field(() => Date, { description: 'Last time this job ran', nullable: true }) + lastRunAt?: Date; + + @Field(() => String, { description: 'Status of last run', nullable: true }) + lastRunStatus?: string; +} + +@InputType() +export class CreateBackupJobConfigInput { + @Field(() => String) + @IsString() + @IsNotEmpty() + name!: string; + + @Field(() => String) + @IsString() + @IsNotEmpty() + 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; +} + +@InputType() +export class UpdateBackupJobConfigInput { + @Field(() => String, { nullable: true }) + @IsOptional() + @IsString() + @IsNotEmpty() + name?: string; + + @Field(() => String, { nullable: true }) + @IsOptional() + @IsString() + @IsNotEmpty() + sourcePath?: string; + + @Field(() => String, { nullable: true }) + @IsOptional() + @IsString() + @IsNotEmpty() + remoteName?: string; + + @Field(() => String, { nullable: true }) + @IsOptional() + @IsString() + @IsNotEmpty() + destinationPath?: string; + + @Field(() => String, { nullable: true }) + @IsOptional() + @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, { nullable: true }) + @IsOptional() + @IsBoolean() + enabled?: boolean; + + @Field(() => GraphQLJSON, { nullable: true }) + @IsOptional() + @IsObject() + rcloneOptions?: Record; +} + +@ObjectType() +export class BackupJobConfigForm { + @Field(() => ID) + id!: string; + + @Field(() => GraphQLJSON) + dataSchema!: { properties: DataSlice; type: 'object' }; + + @Field(() => GraphQLJSON) + uiSchema!: Layout; +} + +@InputType() +export class BackupJobConfigFormInput { + @Field(() => Boolean, { defaultValue: false }) + @IsOptional() + @IsBoolean() + showAdvanced?: boolean; +} diff --git a/api/src/unraid-api/graph/resolvers/backup/backup.module.ts b/api/src/unraid-api/graph/resolvers/backup/backup.module.ts new file mode 100644 index 000000000..9738e8d6f --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/backup/backup.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { ScheduleModule } from '@nestjs/schedule'; + +import { BackupConfigService } from '@app/unraid-api/graph/resolvers/backup/backup-config.service.js'; +import { BackupResolver } from '@app/unraid-api/graph/resolvers/backup/backup.resolver.js'; +import { FormatService } from '@app/unraid-api/graph/resolvers/backup/format.service.js'; +import { RCloneModule } from '@app/unraid-api/graph/resolvers/rclone/rclone.module.js'; + +@Module({ + imports: [RCloneModule, ScheduleModule.forRoot()], + providers: [BackupResolver, BackupConfigService, FormatService], + exports: [], +}) +export class BackupModule {} diff --git a/api/src/unraid-api/graph/resolvers/backup/backup.resolver.ts b/api/src/unraid-api/graph/resolvers/backup/backup.resolver.ts new file mode 100644 index 000000000..58c95ff96 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/backup/backup.resolver.ts @@ -0,0 +1,201 @@ +import { Inject, Logger } from '@nestjs/common'; +import { Args, Mutation, Query, ResolveField, Resolver } from '@nestjs/graphql'; + +import { BackupConfigService } from '@app/unraid-api/graph/resolvers/backup/backup-config.service.js'; +import { + Backup, + BackupJob, + BackupJobConfig, + BackupJobConfigForm, + BackupJobConfigFormInput, + BackupStatus, + CreateBackupJobConfigInput, + InitiateBackupInput, + UpdateBackupJobConfigInput, +} from '@app/unraid-api/graph/resolvers/backup/backup.model.js'; +import { FormatService } from '@app/unraid-api/graph/resolvers/backup/format.service.js'; +import { buildBackupJobConfigSchema } from '@app/unraid-api/graph/resolvers/backup/jsonforms/backup-jsonforms-config.js'; +import { RCloneService } from '@app/unraid-api/graph/resolvers/rclone/rclone.service.js'; + +@Resolver(() => Backup) +export class BackupResolver { + private readonly logger = new Logger(BackupResolver.name); + + constructor( + private readonly rcloneService: RCloneService, + private readonly backupConfigService: BackupConfigService, + private readonly formatService: FormatService + ) {} + + @Query(() => Backup, { + description: 'Get backup service information', + }) + async backup(): Promise { + return { + id: 'backup', + jobs: [], + configs: [], + }; + } + + @ResolveField(() => [BackupJob], { + description: 'Get all running backup jobs', + }) + async jobs(): Promise { + return this.backupJobs(); + } + + @ResolveField(() => [BackupJobConfig], { + description: 'Get all backup job configurations', + }) + async configs(): Promise { + return this.backupConfigService.getAllBackupJobConfigs(); + } + + @Query(() => BackupJobConfig, { + description: 'Get a specific backup job configuration', + nullable: true, + }) + async backupJobConfig(@Args('id') id: string): Promise { + return this.backupConfigService.getBackupJobConfig(id); + } + + @Mutation(() => BackupJobConfig, { + description: 'Create a new backup job configuration', + }) + async createBackupJobConfig( + @Args('input') input: CreateBackupJobConfigInput + ): Promise { + return this.backupConfigService.createBackupJobConfig(input); + } + + @Mutation(() => BackupJobConfig, { + description: 'Update a backup job configuration', + nullable: true, + }) + async updateBackupJobConfig( + @Args('id') id: string, + @Args('input') input: UpdateBackupJobConfigInput + ): Promise { + return this.backupConfigService.updateBackupJobConfig(id, input); + } + + @Mutation(() => Boolean, { + description: 'Delete a backup job configuration', + }) + async deleteBackupJobConfig(@Args('id') id: string): Promise { + return this.backupConfigService.deleteBackupJobConfig(id); + } + + private async backupJobs(): Promise { + try { + const jobs = await this.rcloneService['rcloneApiService'].listRunningJobs(); + return ( + jobs.jobids?.map((jobId: string, index: number) => { + const stats = jobs.stats?.[index] || {}; + return { + id: jobId, + type: 'backup', + stats, + formattedBytes: stats.bytes + ? this.formatService.formatBytes(stats.bytes) + : undefined, + formattedSpeed: stats.speed + ? this.formatService.formatBytes(stats.speed) + : undefined, + formattedElapsedTime: stats.elapsedTime + ? this.formatService.formatDuration(stats.elapsedTime) + : undefined, + formattedEta: stats.eta + ? this.formatService.formatDuration(stats.eta) + : undefined, + }; + }) || [] + ); + } catch (error) { + this.logger.error('Failed to fetch backup jobs:', error); + return []; + } + } + + @Query(() => BackupJob, { + description: 'Get status of a specific backup job', + nullable: true, + }) + async backupJob(@Args('jobId') jobId: string): Promise { + try { + const status = await this.rcloneService['rcloneApiService'].getJobStatus({ jobId }); + return { + id: jobId, + type: status.group || 'backup', + stats: status, + formattedBytes: status.bytes ? this.formatService.formatBytes(status.bytes) : undefined, + formattedSpeed: status.speed ? this.formatService.formatBytes(status.speed) : undefined, + formattedElapsedTime: status.elapsedTime + ? this.formatService.formatDuration(status.elapsedTime) + : undefined, + formattedEta: status.eta ? this.formatService.formatDuration(status.eta) : undefined, + }; + } catch (error) { + this.logger.error(`Failed to fetch backup job ${jobId}:`, error); + return null; + } + } + + @Mutation(() => BackupStatus, { + description: 'Initiates a backup using a configured remote.', + }) + async initiateBackup(@Args('input') input: InitiateBackupInput): Promise { + try { + const result = await this.rcloneService['rcloneApiService'].startBackup({ + srcPath: input.sourcePath, + dstPath: `${input.remoteName}:${input.destinationPath}`, + options: input.options, + }); + + return { + status: 'Backup initiated successfully', + jobId: result.jobid || result.jobId, + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + this.logger.error('Failed to initiate backup:', error); + + return { + status: `Failed to initiate backup: ${errorMessage}`, + jobId: undefined, + }; + } + } + + @ResolveField(() => BackupStatus, { + description: 'Get the status for the backup service', + }) + async status(): Promise { + return { + status: 'Available', + jobId: undefined, + }; + } + + @Query(() => BackupJobConfigForm, { + description: 'Get the JSON schema for backup job configuration form', + }) + async backupJobConfigForm( + @Args('input', { nullable: true }) input?: BackupJobConfigFormInput + ): Promise { + const remoteNames = await this.rcloneService.getConfiguredRemotes(); + const showAdvanced = input?.showAdvanced ?? false; + + const { dataSchema, uiSchema } = buildBackupJobConfigSchema({ + remoteNames, + showAdvanced, + }); + + return { + id: 'backup-job-config-form', + dataSchema, + uiSchema, + }; + } +} diff --git a/api/src/unraid-api/graph/resolvers/backup/format.service.ts b/api/src/unraid-api/graph/resolvers/backup/format.service.ts new file mode 100644 index 000000000..f775f1cb2 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/backup/format.service.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@nestjs/common'; + +import { convert } from 'convert'; + +@Injectable() +export class FormatService { + formatBytes(bytes: number): string { + if (bytes === 0) return '0 B'; + + const result = convert(bytes, 'bytes').to('best'); + return result.toString(); + } + + formatDuration(seconds: number): string { + if (seconds < 60) return `${seconds}s`; + + const result = convert(seconds, 'seconds').to('best'); + return result.toString(); + } +} diff --git a/api/src/unraid-api/graph/resolvers/backup/jsonforms/backup-jsonforms-config.ts b/api/src/unraid-api/graph/resolvers/backup/jsonforms/backup-jsonforms-config.ts new file mode 100644 index 000000000..4e732bc0c --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/backup/jsonforms/backup-jsonforms-config.ts @@ -0,0 +1,346 @@ +import type { LabelElement, Layout } from '@jsonforms/core'; +import { JsonSchema7 } from '@jsonforms/core'; + +import type { DataSlice, SettingSlice, UIElement } from '@app/unraid-api/types/json-forms.js'; +import { createLabeledControl } from '@app/unraid-api/graph/utils/form-utils.js'; +import { mergeSettingSlices } from '@app/unraid-api/types/json-forms.js'; + +function getBasicBackupConfigSlice({ remoteNames = [] }: { remoteNames?: string[] }): SettingSlice { + const basicConfigElements: UIElement[] = [ + createLabeledControl({ + scope: '#/properties/name', + label: 'Backup Job Name', + description: 'A descriptive name for this backup job (e.g., "Weekly Documents Backup")', + controlOptions: { + placeholder: 'Enter backup job name', + format: 'string', + }, + }), + + createLabeledControl({ + scope: '#/properties/sourcePath', + label: 'Source Path', + description: 'The local path to backup (e.g., /mnt/user/Documents)', + controlOptions: { + placeholder: '/mnt/user/', + format: 'string', + }, + }), + + createLabeledControl({ + scope: '#/properties/remoteName', + label: 'Remote Configuration', + description: 'Select the RClone remote configuration to use for this backup', + controlOptions: { + suggestions: remoteNames.map((name) => ({ + value: name, + label: name, + })), + }, + }), + + createLabeledControl({ + scope: '#/properties/destinationPath', + label: 'Destination Path', + description: 'The path on the remote where files will be stored (e.g., backups/documents)', + controlOptions: { + placeholder: 'backups/', + format: 'string', + }, + }), + + createLabeledControl({ + scope: '#/properties/schedule', + label: 'Schedule (Cron Expression)', + description: + 'When to run this backup job. Examples: "0 2 * * *" (daily at 2AM), "0 2 * * 0" (weekly on Sunday at 2AM)', + controlOptions: { + placeholder: '0 2 * * *', + format: 'string', + suggestions: [ + { + value: '0 2 * * *', + label: 'Daily at 2:00 AM', + tooltip: 'Runs every day at 2:00 AM', + }, + { + value: '0 2 * * 0', + label: 'Weekly (Sunday 2:00 AM)', + tooltip: 'Runs every Sunday at 2:00 AM', + }, + { + value: '0 2 1 * *', + label: 'Monthly (1st at 2:00 AM)', + tooltip: 'Runs on the 1st of every month at 2:00 AM', + }, + { + value: '0 2 * * 1-5', + label: 'Weekdays at 2:00 AM', + tooltip: 'Runs Monday through Friday at 2:00 AM', + }, + ], + }, + }), + + createLabeledControl({ + scope: '#/properties/enabled', + label: 'Enable Backup Job', + description: 'Whether this backup job should run automatically according to the schedule', + controlOptions: { + toggle: true, + }, + }), + + { + type: 'Label', + text: 'Advanced Options', + options: { + description: 'Optional RClone-specific settings for this backup job.', + }, + } as LabelElement, + + createLabeledControl({ + scope: '#/properties/showAdvanced', + label: 'Show Advanced RClone Options', + description: 'Display additional RClone configuration options', + controlOptions: { + toggle: true, + }, + }), + ]; + + const basicConfigProperties: Record = { + name: { + type: 'string', + title: 'Backup Job Name', + description: 'Human-readable name for this backup job', + minLength: 1, + maxLength: 100, + }, + sourcePath: { + type: 'string', + title: 'Source Path', + description: 'Source path to backup', + minLength: 1, + }, + remoteName: { + type: 'string', + title: 'Remote Name', + description: 'Remote name from rclone config', + enum: remoteNames.length > 0 ? remoteNames : ['No remotes configured'], + }, + destinationPath: { + type: 'string', + title: 'Destination Path', + description: 'Destination path on the remote', + minLength: 1, + }, + schedule: { + type: 'string', + title: 'Cron Schedule', + description: 'Cron schedule expression', + pattern: '^\\s*(\\S+\\s+){4}\\S+\\s*$', + errorMessage: + 'Please enter a valid cron expression (5 fields: minute hour day month weekday)', + }, + enabled: { + type: 'boolean', + title: 'Enabled', + description: 'Whether this backup job is enabled', + default: true, + }, + showAdvanced: { + type: 'boolean', + title: 'Show Advanced Options', + description: 'Whether to show advanced RClone options', + default: false, + }, + }; + + const verticalLayoutElement: UIElement = { + type: 'VerticalLayout', + elements: basicConfigElements, + options: { step: 0 }, + }; + + return { + properties: basicConfigProperties as unknown as DataSlice, + elements: [verticalLayoutElement], + }; +} + +function getAdvancedBackupConfigSlice({ showAdvanced }: { showAdvanced: boolean }): SettingSlice { + if (!showAdvanced) { + return { + properties: {}, + elements: [], + }; + } + + const advancedConfigElements: UIElement[] = [ + createLabeledControl({ + scope: '#/properties/rcloneOptions/properties/transfers', + label: 'Number of Transfers', + description: 'Number of file transfers to run in parallel (default: 4)', + controlOptions: { + placeholder: '4', + format: 'number', + }, + }), + + createLabeledControl({ + scope: '#/properties/rcloneOptions/properties/checkers', + label: 'Number of Checkers', + description: 'Number of checkers to run in parallel (default: 8)', + controlOptions: { + placeholder: '8', + format: 'number', + }, + }), + + createLabeledControl({ + scope: '#/properties/rcloneOptions/properties/dryRun', + label: 'Dry Run', + description: 'Do a trial run with no permanent changes', + controlOptions: { + toggle: true, + }, + }), + + createLabeledControl({ + scope: '#/properties/rcloneOptions/properties/progress', + label: 'Show Progress', + description: 'Show progress during transfer', + controlOptions: { + toggle: true, + }, + }), + + createLabeledControl({ + scope: '#/properties/rcloneOptions/properties/verbose', + label: 'Verbose Logging', + description: 'Enable verbose logging for debugging', + controlOptions: { + toggle: true, + }, + }), + ]; + + const rcloneOptionsProperties: Record = { + transfers: { + type: 'integer', + title: 'Transfers', + description: 'Number of file transfers to run in parallel', + minimum: 1, + maximum: 100, + default: 4, + }, + checkers: { + type: 'integer', + title: 'Checkers', + description: 'Number of checkers to run in parallel', + minimum: 1, + maximum: 100, + default: 8, + }, + dryRun: { + type: 'boolean', + title: 'Dry Run', + description: 'Do a trial run with no permanent changes', + default: false, + }, + progress: { + type: 'boolean', + title: 'Show Progress', + description: 'Show progress during transfer', + default: true, + }, + verbose: { + type: 'boolean', + title: 'Verbose Logging', + description: 'Enable verbose logging', + default: false, + }, + }; + + const configProperties: DataSlice = { + rcloneOptions: { + type: 'object', + title: 'RClone Options', + description: 'Advanced RClone configuration options', + properties: rcloneOptionsProperties as unknown as DataSlice, + } as any, + }; + + const verticalLayoutElement: UIElement = { + type: 'VerticalLayout', + elements: advancedConfigElements, + options: { step: 1, showDividers: true }, + }; + + return { + properties: configProperties, + elements: [verticalLayoutElement], + }; +} + +export function buildBackupJobConfigSchema({ + remoteNames = [], + showAdvanced = false, +}: { + remoteNames?: string[]; + showAdvanced?: boolean; +}): { + dataSchema: { properties: DataSlice; type: 'object' }; + uiSchema: Layout; +} { + const slicesToMerge: SettingSlice[] = []; + + const basicSlice = getBasicBackupConfigSlice({ remoteNames }); + slicesToMerge.push(basicSlice); + + const advancedSlice = getAdvancedBackupConfigSlice({ showAdvanced }); + if ( + showAdvanced && + (advancedSlice.elements.length > 0 || Object.keys(advancedSlice.properties).length > 0) + ) { + slicesToMerge.push(advancedSlice); + } + + const mergedSlices = mergeSettingSlices(slicesToMerge); + + const dataSchema: { properties: DataSlice; type: 'object' } = { + type: 'object', + properties: mergedSlices.properties, + }; + + const steps = [{ label: 'Backup Configuration', description: 'Basic backup job settings' }]; + + if (showAdvanced) { + steps.push({ label: 'Advanced Options', description: 'RClone-specific settings' }); + } + + const steppedLayoutElement: UIElement = { + type: 'SteppedLayout', + options: { + steps: steps, + }, + elements: mergedSlices.elements, + }; + + const titleLabel: UIElement = { + type: 'Label', + text: 'Create Backup Job', + options: { + format: 'title', + description: 'Configure a new scheduled backup job with RClone.', + }, + }; + + const uiSchema: Layout = { + type: 'VerticalLayout', + elements: [titleLabel, steppedLayoutElement], + }; + + return { dataSchema, uiSchema }; +} diff --git a/api/src/unraid-api/graph/resolvers/flash-backup/flash-backup.model.ts b/api/src/unraid-api/graph/resolvers/flash-backup/flash-backup.model.ts deleted file mode 100644 index 5013eaf3a..000000000 --- a/api/src/unraid-api/graph/resolvers/flash-backup/flash-backup.model.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Field, InputType, ObjectType } from '@nestjs/graphql'; - -import { GraphQLJSON } from 'graphql-scalars'; - -@InputType() -export class InitiateFlashBackupInput { - @Field(() => String, { description: 'The name of the remote configuration to use for the backup.' }) - remoteName!: string; - - @Field(() => String, { description: 'Source path to backup (typically the flash drive).' }) - sourcePath!: string; - - @Field(() => String, { description: 'Destination path on the remote.' }) - destinationPath!: string; - - @Field(() => GraphQLJSON, { - description: 'Additional options for the backup operation, such as --dry-run or --transfers.', - nullable: true, - }) - options?: Record; -} - -@ObjectType() -export class FlashBackupStatus { - @Field(() => String, { - description: 'Status message indicating the outcome of the backup initiation.', - }) - status!: string; - - @Field(() => String, { - description: 'Job ID if available, can be used to check job status.', - nullable: true, - }) - jobId?: string; -} - -@ObjectType() -export class FlashBackupJob { - @Field(() => String, { description: 'Job ID' }) - id!: string; - - @Field(() => String, { description: 'Job type (e.g., sync/copy)' }) - type!: string; - - @Field(() => GraphQLJSON, { description: 'Job status and statistics' }) - stats!: Record; -} - -@ObjectType() -export class RCloneWebGuiInfo { - @Field() - url!: string; -} diff --git a/api/src/unraid-api/graph/resolvers/flash-backup/flash-backup.module.ts b/api/src/unraid-api/graph/resolvers/flash-backup/flash-backup.module.ts deleted file mode 100644 index 3b9fe0dcb..000000000 --- a/api/src/unraid-api/graph/resolvers/flash-backup/flash-backup.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { FlashBackupResolver } from '@app/unraid-api/graph/resolvers/flash-backup/flash-backup.resolver.js'; -import { RCloneModule } from '@app/unraid-api/graph/resolvers/rclone/rclone.module.js'; - -@Module({ - imports: [RCloneModule], - providers: [FlashBackupResolver], - exports: [], -}) -export class FlashBackupModule {} diff --git a/api/src/unraid-api/graph/resolvers/flash-backup/flash-backup.resolver.ts b/api/src/unraid-api/graph/resolvers/flash-backup/flash-backup.resolver.ts deleted file mode 100644 index 6358fb30e..000000000 --- a/api/src/unraid-api/graph/resolvers/flash-backup/flash-backup.resolver.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Inject, Logger } from '@nestjs/common'; -import { Args, Mutation, Resolver } from '@nestjs/graphql'; - -import { - FlashBackupStatus, - InitiateFlashBackupInput, -} from '@app/unraid-api/graph/resolvers/flash-backup/flash-backup.model.js'; -import { RCloneService } from '@app/unraid-api/graph/resolvers/rclone/rclone.service.js'; - -@Resolver() -export class FlashBackupResolver { - private readonly logger = new Logger(FlashBackupResolver.name); - - constructor(private readonly rcloneService: RCloneService) {} - - @Mutation(() => FlashBackupStatus, { - description: 'Initiates a flash drive backup using a configured remote.', - }) - async initiateFlashBackup( - @Args('input') input: InitiateFlashBackupInput - ): Promise { - throw new Error('Not implemented'); - } -} diff --git a/api/src/unraid-api/graph/resolvers/resolvers.module.ts b/api/src/unraid-api/graph/resolvers/resolvers.module.ts index 04bd2dc60..66a1f3ada 100644 --- a/api/src/unraid-api/graph/resolvers/resolvers.module.ts +++ b/api/src/unraid-api/graph/resolvers/resolvers.module.ts @@ -4,6 +4,7 @@ import { AuthModule } from '@app/unraid-api/auth/auth.module.js'; import { ApiKeyModule } from '@app/unraid-api/graph/resolvers/api-key/api-key.module.js'; import { ApiKeyResolver } from '@app/unraid-api/graph/resolvers/api-key/api-key.resolver.js'; import { ArrayModule } from '@app/unraid-api/graph/resolvers/array/array.module.js'; +import { BackupModule } from '@app/unraid-api/graph/resolvers/backup/backup.module.js'; import { CloudResolver } from '@app/unraid-api/graph/resolvers/cloud/cloud.resolver.js'; import { ConfigResolver } from '@app/unraid-api/graph/resolvers/config/config.resolver.js'; import { ConnectModule } from '@app/unraid-api/graph/resolvers/connect/connect.module.js'; @@ -11,7 +12,6 @@ import { CustomizationModule } from '@app/unraid-api/graph/resolvers/customizati import { DisksModule } from '@app/unraid-api/graph/resolvers/disks/disks.module.js'; import { DisplayResolver } from '@app/unraid-api/graph/resolvers/display/display.resolver.js'; import { DockerModule } from '@app/unraid-api/graph/resolvers/docker/docker.module.js'; -import { FlashBackupModule } from '@app/unraid-api/graph/resolvers/flash-backup/flash-backup.module.js'; import { FlashResolver } from '@app/unraid-api/graph/resolvers/flash/flash.resolver.js'; import { InfoResolver } from '@app/unraid-api/graph/resolvers/info/info.resolver.js'; import { LogsResolver } from '@app/unraid-api/graph/resolvers/logs/logs.resolver.js'; @@ -38,11 +38,11 @@ import { MeResolver } from '@app/unraid-api/graph/user/user.resolver.js'; ArrayModule, ApiKeyModule, AuthModule, + BackupModule, ConnectModule, CustomizationModule, DockerModule, DisksModule, - FlashBackupModule, RCloneModule, ], providers: [ diff --git a/web/components/Backup/BackupJobConfig.vue b/web/components/Backup/BackupJobConfig.vue new file mode 100644 index 000000000..3e3c7b25d --- /dev/null +++ b/web/components/Backup/BackupJobConfig.vue @@ -0,0 +1,183 @@ + + + + + \ No newline at end of file diff --git a/web/components/Backup/BackupJobConfigForm.vue b/web/components/Backup/BackupJobConfigForm.vue new file mode 100644 index 000000000..0f80ae362 --- /dev/null +++ b/web/components/Backup/BackupJobConfigForm.vue @@ -0,0 +1,240 @@ + + + + + \ No newline at end of file diff --git a/web/components/Backup/BackupOverview.vue b/web/components/Backup/BackupOverview.vue new file mode 100644 index 000000000..93682d102 --- /dev/null +++ b/web/components/Backup/BackupOverview.vue @@ -0,0 +1,170 @@ + + + + + diff --git a/web/components/Backup/backup-jobs.query.ts b/web/components/Backup/backup-jobs.query.ts new file mode 100644 index 000000000..b1b194670 --- /dev/null +++ b/web/components/Backup/backup-jobs.query.ts @@ -0,0 +1,75 @@ +import { graphql } from '~/composables/gql/gql'; + +export const BACKUP_JOBS_QUERY = graphql(/* GraphQL */ ` + query BackupJobs { + backup { + id + jobs { + id + type + stats + formattedBytes + formattedSpeed + formattedElapsedTime + formattedEta + } + } + } +`); + +export const BACKUP_JOB_QUERY = graphql(/* GraphQL */ ` + query BackupJob($jobId: String!) { + backupJob(jobId: $jobId) { + id + type + stats + } + } +`); + +export const BACKUP_JOB_CONFIGS_QUERY = graphql(/* GraphQL */ ` + query BackupJobConfigs { + backup { + id + configs { + id + name + sourcePath + remoteName + destinationPath + schedule + enabled + createdAt + updatedAt + lastRunAt + lastRunStatus + } + } + } +`); + +export const BACKUP_JOB_CONFIG_FORM_QUERY = graphql(/* GraphQL */ ` + query BackupJobConfigForm($input: BackupJobConfigFormInput) { + backupJobConfigForm(input: $input) { + id + dataSchema + uiSchema + } + } +`); + +export const CREATE_BACKUP_JOB_CONFIG_MUTATION = graphql(/* GraphQL */ ` + mutation CreateBackupJobConfig($input: CreateBackupJobConfigInput!) { + createBackupJobConfig(input: $input) { + id + name + sourcePath + remoteName + destinationPath + schedule + enabled + createdAt + updatedAt + } + } +`); diff --git a/web/composables/gql/gql.ts b/web/composables/gql/gql.ts index 1a4a4893b..17bb7b6f0 100644 --- a/web/composables/gql/gql.ts +++ b/web/composables/gql/gql.ts @@ -20,6 +20,11 @@ type Documents = { "\n mutation CreateApiKey($input: CreateApiKeyInput!) {\n apiKey {\n create(input: $input) {\n id\n key\n name\n description\n createdAt\n roles\n permissions {\n resource\n actions\n }\n }\n }\n }\n": typeof types.CreateApiKeyDocument, "\n mutation DeleteApiKey($input: DeleteApiKeyInput!) {\n apiKey {\n delete(input: $input)\n }\n }\n": typeof types.DeleteApiKeyDocument, "\n query ApiKeyMeta {\n apiKeyPossibleRoles\n apiKeyPossiblePermissions {\n resource\n actions\n }\n }\n": typeof types.ApiKeyMetaDocument, + "\n query BackupJobs {\n backup {\n id\n jobs {\n id\n type\n stats\n formattedBytes\n formattedSpeed\n formattedElapsedTime\n formattedEta\n }\n }\n }\n": typeof types.BackupJobsDocument, + "\n query BackupJob($jobId: String!) {\n backupJob(jobId: $jobId) {\n id\n type\n stats\n }\n }\n": typeof types.BackupJobDocument, + "\n query BackupJobConfigs {\n backup {\n id\n configs {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n }\n }\n }\n": typeof types.BackupJobConfigsDocument, + "\n query BackupJobConfigForm($input: BackupJobConfigFormInput) {\n backupJobConfigForm(input: $input) {\n id\n dataSchema\n uiSchema\n }\n }\n": typeof types.BackupJobConfigFormDocument, + "\n mutation CreateBackupJobConfig($input: CreateBackupJobConfigInput!) {\n createBackupJobConfig(input: $input) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n }\n }\n": typeof types.CreateBackupJobConfigDocument, "\n query GetConnectSettingsForm {\n connect {\n id\n settings {\n id\n dataSchema\n uiSchema\n values {\n sandbox\n extraOrigins\n accessType\n forwardType\n port\n ssoUserIds\n }\n }\n }\n }\n": typeof types.GetConnectSettingsFormDocument, "\n mutation UpdateConnectSettings($input: ApiSettingsInput!) {\n updateApiSettings(input: $input) {\n sandbox\n extraOrigins\n accessType\n forwardType\n port\n ssoUserIds\n }\n }\n": typeof types.UpdateConnectSettingsDocument, "\n query LogFiles {\n logFiles {\n name\n path\n size\n modifiedAt\n }\n }\n": typeof types.LogFilesDocument, @@ -57,6 +62,11 @@ const documents: Documents = { "\n mutation CreateApiKey($input: CreateApiKeyInput!) {\n apiKey {\n create(input: $input) {\n id\n key\n name\n description\n createdAt\n roles\n permissions {\n resource\n actions\n }\n }\n }\n }\n": types.CreateApiKeyDocument, "\n mutation DeleteApiKey($input: DeleteApiKeyInput!) {\n apiKey {\n delete(input: $input)\n }\n }\n": types.DeleteApiKeyDocument, "\n query ApiKeyMeta {\n apiKeyPossibleRoles\n apiKeyPossiblePermissions {\n resource\n actions\n }\n }\n": types.ApiKeyMetaDocument, + "\n query BackupJobs {\n backup {\n id\n jobs {\n id\n type\n stats\n formattedBytes\n formattedSpeed\n formattedElapsedTime\n formattedEta\n }\n }\n }\n": types.BackupJobsDocument, + "\n query BackupJob($jobId: String!) {\n backupJob(jobId: $jobId) {\n id\n type\n stats\n }\n }\n": types.BackupJobDocument, + "\n query BackupJobConfigs {\n backup {\n id\n configs {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n }\n }\n }\n": types.BackupJobConfigsDocument, + "\n query BackupJobConfigForm($input: BackupJobConfigFormInput) {\n backupJobConfigForm(input: $input) {\n id\n dataSchema\n uiSchema\n }\n }\n": types.BackupJobConfigFormDocument, + "\n mutation CreateBackupJobConfig($input: CreateBackupJobConfigInput!) {\n createBackupJobConfig(input: $input) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n }\n }\n": types.CreateBackupJobConfigDocument, "\n query GetConnectSettingsForm {\n connect {\n id\n settings {\n id\n dataSchema\n uiSchema\n values {\n sandbox\n extraOrigins\n accessType\n forwardType\n port\n ssoUserIds\n }\n }\n }\n }\n": types.GetConnectSettingsFormDocument, "\n mutation UpdateConnectSettings($input: ApiSettingsInput!) {\n updateApiSettings(input: $input) {\n sandbox\n extraOrigins\n accessType\n forwardType\n port\n ssoUserIds\n }\n }\n": types.UpdateConnectSettingsDocument, "\n query LogFiles {\n logFiles {\n name\n path\n size\n modifiedAt\n }\n }\n": types.LogFilesDocument, @@ -126,6 +136,26 @@ export function graphql(source: "\n mutation DeleteApiKey($input: DeleteApiKeyI * 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 ApiKeyMeta {\n apiKeyPossibleRoles\n apiKeyPossiblePermissions {\n resource\n actions\n }\n }\n"): (typeof documents)["\n query ApiKeyMeta {\n apiKeyPossibleRoles\n apiKeyPossiblePermissions {\n resource\n actions\n }\n }\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 BackupJobs {\n backup {\n id\n jobs {\n id\n type\n stats\n formattedBytes\n formattedSpeed\n formattedElapsedTime\n formattedEta\n }\n }\n }\n"): (typeof documents)["\n query BackupJobs {\n backup {\n id\n jobs {\n id\n type\n stats\n formattedBytes\n formattedSpeed\n formattedElapsedTime\n formattedEta\n }\n }\n }\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 BackupJob($jobId: String!) {\n backupJob(jobId: $jobId) {\n id\n type\n stats\n }\n }\n"): (typeof documents)["\n query BackupJob($jobId: String!) {\n backupJob(jobId: $jobId) {\n id\n type\n stats\n }\n }\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 BackupJobConfigs {\n backup {\n id\n configs {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n }\n }\n }\n"): (typeof documents)["\n query BackupJobConfigs {\n backup {\n id\n configs {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n }\n }\n }\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 BackupJobConfigForm($input: BackupJobConfigFormInput) {\n backupJobConfigForm(input: $input) {\n id\n dataSchema\n uiSchema\n }\n }\n"): (typeof documents)["\n query BackupJobConfigForm($input: BackupJobConfigFormInput) {\n backupJobConfigForm(input: $input) {\n id\n dataSchema\n uiSchema\n }\n }\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 mutation CreateBackupJobConfig($input: CreateBackupJobConfigInput!) {\n createBackupJobConfig(input: $input) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n }\n }\n"): (typeof documents)["\n mutation CreateBackupJobConfig($input: CreateBackupJobConfigInput!) {\n createBackupJobConfig(input: $input) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/web/composables/gql/graphql.ts b/web/composables/gql/graphql.ts index 7e2759e68..953688906 100644 --- a/web/composables/gql/graphql.ts +++ b/web/composables/gql/graphql.ts @@ -373,6 +373,79 @@ export enum AuthPossession { OWN_ANY = 'OWN_ANY' } +export type Backup = Node & { + __typename?: 'Backup'; + configs: Array; + id: Scalars['PrefixedID']['output']; + jobs: Array; + /** Get the status for the backup service */ + status: BackupStatus; +}; + +export type BackupJob = { + __typename?: 'BackupJob'; + /** Formatted bytes transferred */ + formattedBytes?: Maybe; + /** Formatted elapsed time */ + formattedElapsedTime?: Maybe; + /** Formatted ETA */ + formattedEta?: Maybe; + /** Formatted transfer speed */ + formattedSpeed?: Maybe; + /** Job ID */ + id: Scalars['String']['output']; + /** Job status and statistics */ + stats: Scalars['JSON']['output']; + /** Job type (e.g., sync/copy) */ + type: Scalars['String']['output']; +}; + +export type BackupJobConfig = { + __typename?: 'BackupJobConfig'; + /** When this config was created */ + createdAt: Scalars['DateTime']['output']; + /** Destination path on the remote */ + destinationPath: Scalars['String']['output']; + /** Whether this backup job is enabled */ + enabled: Scalars['Boolean']['output']; + id: Scalars['PrefixedID']['output']; + /** Last time this job ran */ + lastRunAt?: Maybe; + /** Status of last run */ + lastRunStatus?: Maybe; + /** Human-readable name for this backup job */ + name: Scalars['String']['output']; + /** RClone options (e.g., --transfers, --checkers) */ + rcloneOptions?: Maybe; + /** Remote name from rclone config */ + remoteName: Scalars['String']['output']; + /** Cron schedule expression (e.g., "0 2 * * *" for daily at 2AM) */ + schedule: Scalars['String']['output']; + /** Source path to backup */ + sourcePath: Scalars['String']['output']; + /** When this config was last updated */ + updatedAt: Scalars['DateTime']['output']; +}; + +export type BackupJobConfigForm = { + __typename?: 'BackupJobConfigForm'; + dataSchema: Scalars['JSON']['output']; + id: Scalars['ID']['output']; + uiSchema: Scalars['JSON']['output']; +}; + +export type BackupJobConfigFormInput = { + showAdvanced?: Scalars['Boolean']['input']; +}; + +export type BackupStatus = { + __typename?: 'BackupStatus'; + /** Job ID if available, can be used to check job status. */ + jobId?: Maybe; + /** Status message indicating the outcome of the backup initiation. */ + status: Scalars['String']['output']; +}; + export type Baseboard = Node & { __typename?: 'Baseboard'; assetTag?: Maybe; @@ -525,6 +598,16 @@ export type CreateApiKeyInput = { roles?: InputMaybe>; }; +export type CreateBackupJobConfigInput = { + destinationPath: Scalars['String']['input']; + enabled?: Scalars['Boolean']['input']; + name: Scalars['String']['input']; + rcloneOptions?: InputMaybe; + remoteName: Scalars['String']['input']; + schedule: Scalars['String']['input']; + sourcePath: Scalars['String']['input']; +}; + export type CreateRCloneRemoteInput = { name: Scalars['String']['input']; parameters: Scalars['JSON']['input']; @@ -760,14 +843,6 @@ export type Flash = Node & { vendor: Scalars['String']['output']; }; -export type FlashBackupStatus = { - __typename?: 'FlashBackupStatus'; - /** Job ID if available, can be used to check job status. */ - jobId?: Maybe; - /** Status message indicating the outcome of the backup initiation. */ - status: Scalars['String']['output']; -}; - export type Gpu = Node & { __typename?: 'Gpu'; blacklisted: Scalars['Boolean']['output']; @@ -844,14 +919,14 @@ export type InfoMemory = Node & { used: Scalars['BigInt']['output']; }; -export type InitiateFlashBackupInput = { +export type InitiateBackupInput = { /** Destination path on the remote. */ destinationPath: Scalars['String']['input']; /** Additional options for the backup operation, such as --dry-run or --transfers. */ options?: InputMaybe; /** The name of the remote configuration to use for the backup. */ remoteName: Scalars['String']['input']; - /** Source path to backup (typically the flash drive). */ + /** Source path to backup. */ sourcePath: Scalars['String']['input']; }; @@ -926,15 +1001,19 @@ export type Mutation = { array: ArrayMutations; connectSignIn: Scalars['Boolean']['output']; connectSignOut: Scalars['Boolean']['output']; + /** Create a new backup job configuration */ + createBackupJobConfig: BackupJobConfig; /** Creates a new notification record */ createNotification: Notification; /** Deletes all archived notifications on server. */ deleteArchivedNotifications: NotificationOverview; + /** Delete a backup job configuration */ + deleteBackupJobConfig: Scalars['Boolean']['output']; deleteNotification: NotificationOverview; docker: DockerMutations; enableDynamicRemoteAccess: Scalars['Boolean']['output']; - /** Initiates a flash drive backup using a configured remote. */ - initiateFlashBackup: FlashBackupStatus; + /** Initiates a backup using a configured remote. */ + initiateBackup: BackupStatus; parityCheck: ParityCheckMutations; rclone: RCloneMutations; /** Reads each notification to recompute & update the overview. */ @@ -947,6 +1026,8 @@ export type Mutation = { /** Marks a notification as unread. */ unreadNotification: Notification; updateApiSettings: ConnectSettingsValues; + /** Update a backup job configuration */ + updateBackupJobConfig?: Maybe; vm: VmMutations; }; @@ -971,11 +1052,21 @@ export type MutationConnectSignInArgs = { }; +export type MutationCreateBackupJobConfigArgs = { + input: CreateBackupJobConfigInput; +}; + + export type MutationCreateNotificationArgs = { input: NotificationData; }; +export type MutationDeleteBackupJobConfigArgs = { + id: Scalars['String']['input']; +}; + + export type MutationDeleteNotificationArgs = { id: Scalars['PrefixedID']['input']; type: NotificationType; @@ -987,8 +1078,8 @@ export type MutationEnableDynamicRemoteAccessArgs = { }; -export type MutationInitiateFlashBackupArgs = { - input: InitiateFlashBackupInput; +export type MutationInitiateBackupArgs = { + input: InitiateBackupInput; }; @@ -1021,6 +1112,12 @@ export type MutationUpdateApiSettingsArgs = { input: ApiSettingsInput; }; + +export type MutationUpdateBackupJobConfigArgs = { + id: Scalars['String']['input']; + input: UpdateBackupJobConfigInput; +}; + export type Network = Node & { __typename?: 'Network'; accessUrls?: Maybe>; @@ -1210,6 +1307,14 @@ export type Query = { apiKeyPossibleRoles: Array; apiKeys: Array; array: UnraidArray; + /** Get backup service information */ + backup: Backup; + /** Get status of a specific backup job */ + backupJob?: Maybe; + /** Get a specific backup job configuration */ + backupJobConfig?: Maybe; + /** Get the JSON schema for backup job configuration form */ + backupJobConfigForm: BackupJobConfigForm; cloud: Cloud; config: Config; connect: Connect; @@ -1252,6 +1357,21 @@ export type QueryApiKeyArgs = { }; +export type QueryBackupJobArgs = { + jobId: Scalars['String']['input']; +}; + + +export type QueryBackupJobConfigArgs = { + id: Scalars['String']['input']; +}; + + +export type QueryBackupJobConfigFormArgs = { + input?: InputMaybe; +}; + + export type QueryDiskArgs = { id: Scalars['PrefixedID']['input']; }; @@ -1588,6 +1708,16 @@ export type UnraidArray = Node & { state: ArrayState; }; +export type UpdateBackupJobConfigInput = { + destinationPath?: InputMaybe; + enabled?: InputMaybe; + name?: InputMaybe; + rcloneOptions?: InputMaybe; + remoteName?: InputMaybe; + schedule?: InputMaybe; + sourcePath?: InputMaybe; +}; + export type Uptime = { __typename?: 'Uptime'; timestamp?: Maybe; @@ -1950,6 +2080,37 @@ export type ApiKeyMetaQueryVariables = Exact<{ [key: string]: never; }>; export type ApiKeyMetaQuery = { __typename?: 'Query', apiKeyPossibleRoles: Array, apiKeyPossiblePermissions: Array<{ __typename?: 'Permission', resource: Resource, actions: Array }> }; +export type BackupJobsQueryVariables = Exact<{ [key: string]: never; }>; + + +export type BackupJobsQuery = { __typename?: 'Query', backup: { __typename?: 'Backup', id: string, jobs: Array<{ __typename?: 'BackupJob', id: string, type: string, stats: any, formattedBytes?: string | null, formattedSpeed?: string | null, formattedElapsedTime?: string | null, formattedEta?: string | null }> } }; + +export type BackupJobQueryVariables = Exact<{ + jobId: Scalars['String']['input']; +}>; + + +export type BackupJobQuery = { __typename?: 'Query', backupJob?: { __typename?: 'BackupJob', id: string, type: string, stats: any } | null }; + +export type BackupJobConfigsQueryVariables = Exact<{ [key: string]: never; }>; + + +export type BackupJobConfigsQuery = { __typename?: 'Query', backup: { __typename?: 'Backup', id: string, configs: Array<{ __typename?: 'BackupJobConfig', id: string, name: string, sourcePath: string, remoteName: string, destinationPath: string, schedule: string, enabled: boolean, createdAt: string, updatedAt: string, lastRunAt?: string | null, lastRunStatus?: string | null }> } }; + +export type BackupJobConfigFormQueryVariables = Exact<{ + input?: InputMaybe; +}>; + + +export type BackupJobConfigFormQuery = { __typename?: 'Query', backupJobConfigForm: { __typename?: 'BackupJobConfigForm', id: string, dataSchema: any, uiSchema: any } }; + +export type CreateBackupJobConfigMutationVariables = Exact<{ + input: CreateBackupJobConfigInput; +}>; + + +export type CreateBackupJobConfigMutation = { __typename?: 'Mutation', createBackupJobConfig: { __typename?: 'BackupJobConfig', id: string, name: string, sourcePath: string, remoteName: string, destinationPath: string, schedule: string, enabled: boolean, createdAt: string, updatedAt: string } }; + export type GetConnectSettingsFormQueryVariables = Exact<{ [key: string]: never; }>; @@ -2146,6 +2307,11 @@ export const ApiKeysDocument = {"kind":"Document","definitions":[{"kind":"Operat export const CreateApiKeyDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateApiKey"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateApiKeyInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"apiKey"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"create"},"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":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"roles"}},{"kind":"Field","name":{"kind":"Name","value":"permissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"resource"}},{"kind":"Field","name":{"kind":"Name","value":"actions"}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const DeleteApiKeyDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteApiKey"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DeleteApiKeyInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"apiKey"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"delete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]}}]} as unknown as DocumentNode; export const ApiKeyMetaDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ApiKeyMeta"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"apiKeyPossibleRoles"}},{"kind":"Field","name":{"kind":"Name","value":"apiKeyPossiblePermissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"resource"}},{"kind":"Field","name":{"kind":"Name","value":"actions"}}]}}]}}]} as unknown as DocumentNode; +export const BackupJobsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"BackupJobs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backup"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"jobs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"stats"}},{"kind":"Field","name":{"kind":"Name","value":"formattedBytes"}},{"kind":"Field","name":{"kind":"Name","value":"formattedSpeed"}},{"kind":"Field","name":{"kind":"Name","value":"formattedElapsedTime"}},{"kind":"Field","name":{"kind":"Name","value":"formattedEta"}}]}}]}}]}}]} as unknown as DocumentNode; +export const BackupJobDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"BackupJob"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"jobId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backupJob"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"jobId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"jobId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"stats"}}]}}]}}]} as unknown as DocumentNode; +export const BackupJobConfigsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"BackupJobConfigs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backup"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"configs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePath"}},{"kind":"Field","name":{"kind":"Name","value":"remoteName"}},{"kind":"Field","name":{"kind":"Name","value":"destinationPath"}},{"kind":"Field","name":{"kind":"Name","value":"schedule"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastRunAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastRunStatus"}}]}}]}}]}}]} as unknown as DocumentNode; +export const BackupJobConfigFormDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"BackupJobConfigForm"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"BackupJobConfigFormInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backupJobConfigForm"},"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":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSchema"}},{"kind":"Field","name":{"kind":"Name","value":"uiSchema"}}]}}]}}]} as unknown as DocumentNode; +export const CreateBackupJobConfigDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateBackupJobConfig"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateBackupJobConfigInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createBackupJobConfig"},"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":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePath"}},{"kind":"Field","name":{"kind":"Name","value":"remoteName"}},{"kind":"Field","name":{"kind":"Name","value":"destinationPath"}},{"kind":"Field","name":{"kind":"Name","value":"schedule"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode; export const GetConnectSettingsFormDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetConnectSettingsForm"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connect"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"settings"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSchema"}},{"kind":"Field","name":{"kind":"Name","value":"uiSchema"}},{"kind":"Field","name":{"kind":"Name","value":"values"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sandbox"}},{"kind":"Field","name":{"kind":"Name","value":"extraOrigins"}},{"kind":"Field","name":{"kind":"Name","value":"accessType"}},{"kind":"Field","name":{"kind":"Name","value":"forwardType"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"ssoUserIds"}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const UpdateConnectSettingsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateConnectSettings"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ApiSettingsInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateApiSettings"},"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":"sandbox"}},{"kind":"Field","name":{"kind":"Name","value":"extraOrigins"}},{"kind":"Field","name":{"kind":"Name","value":"accessType"}},{"kind":"Field","name":{"kind":"Name","value":"forwardType"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"ssoUserIds"}}]}}]}}]} as unknown as DocumentNode; export const LogFilesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"LogFiles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"logFiles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"size"}},{"kind":"Field","name":{"kind":"Name","value":"modifiedAt"}}]}}]}}]} as unknown as DocumentNode; diff --git a/web/pages/flashbackup.vue b/web/pages/flashbackup.vue index 8f984cc3f..4b3756d91 100644 --- a/web/pages/flashbackup.vue +++ b/web/pages/flashbackup.vue @@ -1,5 +1,5 @@