mirror of
https://github.com/unraid/api.git
synced 2026-01-05 16:09:49 -06:00
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced full RClone remote management with creation, deletion, listing, and detailed remote info via a multi-step, schema-driven UI. - Added guided configuration forms supporting advanced and provider-specific options for RClone remotes. - Enabled flash backup initiation through API mutations. - Added new Vue components for RClone configuration, overview, remote item cards, and flash backup page. - Integrated new combobox, stepped layout, control wrapper, label renderer, and improved form renderers with enhanced validation and error display. - Added JSON Forms visibility composable and Unraid settings layout for consistent UI rendering. - **Bug Fixes** - Standardized JSON scalar usage in Docker-related types, replacing `JSONObject` with `JSON`. - **Chores** - Added utility scripts and helpers to manage rclone binary installation and versioning. - Updated build scripts and Storybook configuration for CSS handling and improved developer workflow. - Refactored ESLint config for modularity and enhanced code quality enforcement. - Improved component registration with runtime type checks and error handling. - **Documentation** - Added extensive test coverage for RClone API service, JSON Forms schema merging, and provider config slice generation. - **Style** - Improved UI consistency with new layouts, tooltips on select options, password visibility toggles, and error handling components. - Removed deprecated components and consolidated renderer registrations for JSON Forms. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
246 lines
6.8 KiB
Vue
246 lines
6.8 KiB
Vue
<script lang="ts" setup>
|
|
import { computed, provide, ref, watch } from 'vue';
|
|
import { useMutation, useQuery } from '@vue/apollo-composable';
|
|
|
|
import { Button, jsonFormsRenderers } from '@unraid/ui';
|
|
import { JsonForms } from '@jsonforms/vue';
|
|
|
|
import { CREATE_REMOTE } from '~/components/RClone/graphql/rclone.mutations';
|
|
import { GET_RCLONE_CONFIG_FORM } from '~/components/RClone/graphql/rclone.query';
|
|
import { useUnraidApiStore } from '~/store/unraidApi';
|
|
|
|
const { offlineError: _offlineError, unraidApiStatus: _unraidApiStatus } = useUnraidApiStore();
|
|
|
|
// Define props
|
|
const props = defineProps({
|
|
initialState: {
|
|
type: Object,
|
|
default: null
|
|
}
|
|
});
|
|
|
|
// Define emit events
|
|
const emit = defineEmits(['complete']);
|
|
|
|
// Define types for form state
|
|
interface ConfigStep {
|
|
current: number;
|
|
total: number;
|
|
}
|
|
|
|
// Form state
|
|
const formState = ref(props.initialState || {
|
|
configStep: 0 as number | ConfigStep,
|
|
showAdvanced: false,
|
|
name: '',
|
|
type: '',
|
|
parameters: {},
|
|
});
|
|
|
|
// Use static variables to prevent unnecessary refetches
|
|
const {
|
|
result: formResult,
|
|
loading: formLoading,
|
|
refetch: updateFormSchema,
|
|
} = useQuery(GET_RCLONE_CONFIG_FORM, {
|
|
formOptions: {
|
|
providerType: formState.value.type || '',
|
|
parameters: formState.value.parameters || {},
|
|
showAdvanced: formState.value.showAdvanced || false,
|
|
},
|
|
});
|
|
|
|
// Consolidate both watchers into a single watcher with throttling
|
|
let refetchTimeout: NodeJS.Timeout | null = null;
|
|
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;
|
|
|
|
// Check if we need to refetch
|
|
const shouldRefetch =
|
|
newValue.type !== oldValue.type ||
|
|
newStep !== oldStep ||
|
|
newValue.showAdvanced !== oldValue.showAdvanced;
|
|
|
|
if (shouldRefetch) {
|
|
// Log only meaningful changes
|
|
if (newValue.type !== oldValue.type) {
|
|
console.log('[RCloneConfig] providerType changed:', newValue.type);
|
|
}
|
|
|
|
if (newStep !== oldStep || newValue.showAdvanced !== oldValue.showAdvanced) {
|
|
console.log('[RCloneConfig] Refetching form schema');
|
|
}
|
|
|
|
// Debounce refetch to prevent multiple rapid calls
|
|
if (refetchTimeout) {
|
|
clearTimeout(refetchTimeout);
|
|
}
|
|
|
|
refetchTimeout = setTimeout(async () => {
|
|
await updateFormSchema({
|
|
formOptions: {
|
|
providerType: newValue.type,
|
|
parameters: newValue.parameters,
|
|
showAdvanced: newValue.showAdvanced,
|
|
},
|
|
});
|
|
refetchTimeout = null;
|
|
}, 100);
|
|
}
|
|
},
|
|
{ deep: true }
|
|
);
|
|
|
|
/**
|
|
* Form submission and mutation handling
|
|
*/
|
|
const {
|
|
mutate: createRemote,
|
|
loading: isCreating,
|
|
error: createError,
|
|
onDone: onCreateDone,
|
|
} = useMutation(CREATE_REMOTE);
|
|
|
|
// Handle form submission
|
|
const submitForm = async () => {
|
|
try {
|
|
await createRemote({
|
|
input: {
|
|
name: formState.value.name,
|
|
type: formState.value.type,
|
|
parameters: formState.value.parameters,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error('Error creating remote:', error);
|
|
}
|
|
};
|
|
|
|
// Handle successful creation
|
|
onCreateDone(async ({ data }) => {
|
|
// Show success message
|
|
if (window.toast) {
|
|
window.toast.success('Remote Configuration Created', {
|
|
description: `Successfully created remote "${formState.value.name}"`,
|
|
});
|
|
}
|
|
|
|
console.log('[RCloneConfig] onCreateDone', data);
|
|
|
|
// Reset form and emit complete event
|
|
formState.value = {
|
|
configStep: 0,
|
|
showAdvanced: false,
|
|
name: '',
|
|
type: '',
|
|
parameters: {},
|
|
};
|
|
|
|
emit('complete');
|
|
});
|
|
|
|
// Set up JSONForms config
|
|
const jsonFormsConfig = {
|
|
restrict: false,
|
|
trim: false,
|
|
};
|
|
|
|
const renderers = [...jsonFormsRenderers];
|
|
|
|
// Handle form data changes with debouncing to reduce excessive logging
|
|
let changeTimeout: NodeJS.Timeout | null = null;
|
|
const onChange = ({ data }: { data: Record<string, unknown> }) => {
|
|
// Clear any pending timeout
|
|
if (changeTimeout) {
|
|
clearTimeout(changeTimeout);
|
|
}
|
|
|
|
// Log changes but debounce to reduce console spam
|
|
changeTimeout = setTimeout(() => {
|
|
console.log('[RCloneConfig] onChange received data:', JSON.stringify(data));
|
|
changeTimeout = null;
|
|
}, 300);
|
|
|
|
// Update formState
|
|
formState.value = data as typeof formState.value;
|
|
};
|
|
|
|
// --- Submit Button Logic ---
|
|
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;
|
|
});
|
|
|
|
// Get total steps from UI schema
|
|
const numSteps = computed(() => {
|
|
if (uiSchema.value?.type === 'SteppedLayout') {
|
|
return uiSchema.value?.options?.steps?.length ?? 0;
|
|
} else if (uiSchema.value?.elements?.[0]?.type === 'SteppedLayout') {
|
|
return uiSchema.value?.elements[0].options?.steps?.length ?? 0;
|
|
}
|
|
return 0;
|
|
});
|
|
|
|
const isLastStep = computed(() => {
|
|
if (numSteps.value === 0) return false;
|
|
return getCurrentStep.value === numSteps.value - 1;
|
|
});
|
|
|
|
// --- Provide submission logic to SteppedLayout ---
|
|
provide('submitForm', submitForm);
|
|
provide('isSubmitting', isCreating);
|
|
</script>
|
|
|
|
<template>
|
|
<div class="bg-white rounded-lg border border-gray-200 shadow-sm">
|
|
<div class="p-6">
|
|
<h2 class="text-xl font-medium mb-4">Configure RClone Remote</h2>
|
|
|
|
<div v-if="createError" class="mb-4 p-4 bg-red-50 border border-red-200 text-red-700 rounded-md">
|
|
{{ createError.message }}
|
|
</div>
|
|
|
|
<div v-if="formLoading" class="py-8 text-center text-gray-500">Loading configuration form...</div>
|
|
|
|
<!-- Form -->
|
|
<div v-else-if="formResult?.rclone?.configForm" class="mt-6 [&_.vertical-layout]:space-y-6">
|
|
<JsonForms
|
|
v-if="formResult?.rclone?.configForm"
|
|
:schema="formResult.rclone.configForm.dataSchema"
|
|
:uischema="formResult.rclone.configForm.uiSchema"
|
|
:renderers="renderers"
|
|
:data="formState"
|
|
:config="jsonFormsConfig"
|
|
:readonly="isCreating"
|
|
@change="onChange"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Submit Button (visible only on the last step) -->
|
|
<div
|
|
v-if="!formLoading && uiSchema && isLastStep"
|
|
class="mt-6 flex justify-end border-t border-gray-200 pt-6"
|
|
>
|
|
<Button :loading="isCreating" @click="submitForm"> Submit Configuration </Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="postcss">
|
|
/* Import unraid-ui globals first */
|
|
@import '@unraid/ui/styles';
|
|
</style>
|