mirror of
https://github.com/unraid/api.git
synced 2025-12-31 13:39:52 -06:00
restart when developer sandbox is toggled (#1232)
When the sandbox is toggled via api, the api now restarts after a 3 second delay. The Connect settings UI also informs users, when applicable, that the api will restart before and after they apply their settings. ## Summary by CodeRabbit - **New Features** - Improved deployment commands now allow specifying a target server, streamlining the deployment process. - Enhanced settings synchronization provides clear feedback on when a system restart is required after updates. - Automatic service restart is now triggered after applying connection settings changes. - User interface enhancements include added contextual descriptions for toggle controls. - New functionality to refetch connection settings after updates, providing users with the latest information. - **Bug Fixes** - Improved user feedback regarding API restart status after settings updates.
This commit is contained in:
@@ -9,8 +9,8 @@ default:
|
||||
pnpm run build
|
||||
|
||||
# deploys to an unraid server
|
||||
@deploy:
|
||||
./scripts/deploy-dev.sh
|
||||
@deploy remote:
|
||||
./scripts/deploy-dev.sh {{remote}}
|
||||
|
||||
# build & deploy
|
||||
bd: build deploy
|
||||
alias b := build
|
||||
alias d := deploy
|
||||
|
||||
@@ -61,8 +61,10 @@ export class ConnectSettingsService {
|
||||
/**
|
||||
* Syncs the settings to the store and writes the config to disk
|
||||
* @param settings - The settings to sync
|
||||
* @returns true if a restart is required, false otherwise
|
||||
*/
|
||||
async syncSettings(settings: Partial<ApiSettingsInput>) {
|
||||
let restartRequired = false;
|
||||
const { getters } = await import('@app/store/index.js');
|
||||
const { nginx } = getters.emhttp();
|
||||
if (settings.accessType === WAN_ACCESS_TYPE.DISABLED) {
|
||||
@@ -89,10 +91,11 @@ export class ConnectSettingsService {
|
||||
await this.updateAllowedOrigins(settings.extraOrigins);
|
||||
}
|
||||
if (typeof settings.sandbox === 'boolean') {
|
||||
await this.setSandboxMode(settings.sandbox);
|
||||
restartRequired ||= await this.setSandboxMode(settings.sandbox);
|
||||
}
|
||||
const { writeConfigSync } = await import('@app/store/sync/config-disk-sync.js');
|
||||
writeConfigSync('flash');
|
||||
return restartRequired;
|
||||
}
|
||||
|
||||
private async updateAllowedOrigins(origins: string[]) {
|
||||
@@ -100,9 +103,18 @@ export class ConnectSettingsService {
|
||||
store.dispatch(updateAllowedOrigins(origins));
|
||||
}
|
||||
|
||||
private async setSandboxMode(sandbox: boolean) {
|
||||
const { store } = await import('@app/store/index.js');
|
||||
store.dispatch(updateUserConfig({ local: { sandbox: sandbox ? 'yes' : 'no' } }));
|
||||
/**
|
||||
* Sets the sandbox mode and returns true if the mode was changed
|
||||
* @param sandboxEnabled - Whether to enable sandbox mode
|
||||
* @returns true if the mode was changed, false otherwise
|
||||
*/
|
||||
private async setSandboxMode(sandboxEnabled: boolean): Promise<boolean> {
|
||||
const { store, getters } = await import('@app/store/index.js');
|
||||
const currentSandbox = getters.config().local.sandbox;
|
||||
const sandbox = sandboxEnabled ? 'yes' : 'no';
|
||||
if (currentSandbox === sandbox) return false;
|
||||
store.dispatch(updateUserConfig({ local: { sandbox } }));
|
||||
return true;
|
||||
}
|
||||
|
||||
private async updateRemoteAccess(input: SetupRemoteAccessInput): Promise<boolean> {
|
||||
@@ -137,7 +149,7 @@ export class ConnectSettingsService {
|
||||
async buildSettingsSchema(): Promise<SettingSlice> {
|
||||
const slices = [
|
||||
await this.remoteAccessSlice(),
|
||||
this.sandboxSlice(),
|
||||
await this.sandboxSlice(),
|
||||
this.flashBackupSlice(),
|
||||
// Because CORS is effectively disabled, this setting is no longer necessary
|
||||
// keeping it here for in case it needs to be re-enabled
|
||||
@@ -257,7 +269,10 @@ export class ConnectSettingsService {
|
||||
/**
|
||||
* Developer sandbox settings slice
|
||||
*/
|
||||
sandboxSlice(): SettingSlice {
|
||||
async sandboxSlice(): Promise<SettingSlice> {
|
||||
const { sandbox } = await this.getCurrentSettings();
|
||||
const description =
|
||||
'The developer sandbox is available at <code><a class="underline" href="/graphql" target="_blank">/graphql</a></code>.';
|
||||
return {
|
||||
properties: {
|
||||
sandbox: {
|
||||
@@ -273,6 +288,7 @@ export class ConnectSettingsService {
|
||||
label: 'Enable Developer Sandbox:',
|
||||
options: {
|
||||
toggle: true,
|
||||
description: sandbox ? description : undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -16,11 +16,15 @@ import { RemoteAccessController } from '@app/remoteAccess/remote-access-controll
|
||||
import { store } from '@app/store/index.js';
|
||||
import { setAllowedRemoteAccessUrl } from '@app/store/modules/dynamic-remote-access.js';
|
||||
import { ConnectSettingsService } from '@app/unraid-api/graph/connect/connect-settings.service.js';
|
||||
import { ConnectService } from '@app/unraid-api/graph/connect/connect.service.js';
|
||||
|
||||
@Resolver('Connect')
|
||||
export class ConnectResolver implements ConnectResolvers {
|
||||
protected logger = new Logger(ConnectResolver.name);
|
||||
constructor(private readonly connectSettingsService: ConnectSettingsService) {}
|
||||
constructor(
|
||||
private readonly connectSettingsService: ConnectSettingsService,
|
||||
private readonly connectService: ConnectService
|
||||
) {}
|
||||
|
||||
@Query('connect')
|
||||
@UsePermissions({
|
||||
@@ -62,8 +66,16 @@ export class ConnectResolver implements ConnectResolvers {
|
||||
})
|
||||
public async updateApiSettings(@Args('input') settings: ApiSettingsInput) {
|
||||
this.logger.verbose(`Attempting to update API settings: ${JSON.stringify(settings, null, 2)}`);
|
||||
await this.connectSettingsService.syncSettings(settings);
|
||||
return this.connectSettingsService.getCurrentSettings();
|
||||
const restartRequired = await this.connectSettingsService.syncSettings(settings);
|
||||
const currentSettings = await this.connectSettingsService.getCurrentSettings();
|
||||
if (restartRequired) {
|
||||
const restartDelayMs = 3_000;
|
||||
setTimeout(async () => {
|
||||
this.logger.log('Restarting API');
|
||||
await this.connectService.restartApi();
|
||||
}, restartDelayMs);
|
||||
}
|
||||
return currentSettings;
|
||||
}
|
||||
|
||||
@ResolveField()
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { execa } from 'execa';
|
||||
|
||||
@Injectable()
|
||||
export class ConnectService {}
|
||||
export class ConnectService {
|
||||
private logger = new Logger(ConnectService.name);
|
||||
async restartApi() {
|
||||
try {
|
||||
await execa('unraid-api', ['restart'], { shell: 'bash', stdio: 'ignore' });
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { Switch as UuiSwitch } from '@/components/form/switch';
|
||||
import { useJsonFormsControl } from '@jsonforms/vue';
|
||||
|
||||
@@ -12,10 +14,12 @@ const { control, handleChange } = useJsonFormsControl(props);
|
||||
const onChange = (checked: boolean) => {
|
||||
handleChange(control.value.path, checked);
|
||||
};
|
||||
const description = computed(() => props.uischema.options?.description);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ControlLayout v-if="control.visible" :label="control.label" :errors="control.errors">
|
||||
<p v-if="description" v-html="description" class="mb-2"></p>
|
||||
<UuiSwitch
|
||||
:id="control.id + '-input'"
|
||||
:name="control.path"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { useMutation, useQuery } from '@vue/apollo-composable';
|
||||
|
||||
import { BrandButton, Label, jsonFormsRenderers } from '@unraid/ui';
|
||||
import { BrandButton, jsonFormsRenderers, Label } from '@unraid/ui';
|
||||
import { JsonForms } from '@jsonforms/vue';
|
||||
|
||||
import type { ConnectSettingsValues } from '~/composables/gql/graphql';
|
||||
@@ -17,7 +17,7 @@ import { getConnectSettingsForm, updateConnectSettings } from './graphql/setting
|
||||
*---------------------------------------------**/
|
||||
|
||||
const formState = ref<Partial<ConnectSettingsValues>>({});
|
||||
const { result } = useQuery(getConnectSettingsForm);
|
||||
const { result, refetch } = useQuery(getConnectSettingsForm);
|
||||
const settings = computed(() => {
|
||||
if (!result.value) return;
|
||||
return result.value?.connect.settings;
|
||||
@@ -27,6 +27,9 @@ watch(result, () => {
|
||||
const { __typename, ...initialValues } = result.value.connect.settings.values;
|
||||
formState.value = initialValues;
|
||||
});
|
||||
const restartRequired = computed(() => {
|
||||
return settings.value?.values.sandbox !== formState.value?.sandbox;
|
||||
});
|
||||
|
||||
/**--------------------------------------------
|
||||
* Update Settings Actions
|
||||
@@ -54,7 +57,9 @@ watchDebounced(
|
||||
|
||||
// show a toast when the update is done
|
||||
onMutateSettingsDone(() => {
|
||||
globalThis.toast.success('Updated API Settings');
|
||||
globalThis.toast.success('Updated API Settings', {
|
||||
description: restartRequired.value ? 'The API is restarting...' : undefined,
|
||||
});
|
||||
});
|
||||
|
||||
/**--------------------------------------------
|
||||
@@ -66,14 +71,13 @@ const jsonFormsConfig = {
|
||||
trim: false,
|
||||
};
|
||||
|
||||
const renderers = [
|
||||
...jsonFormsRenderers,
|
||||
];
|
||||
const renderers = [...jsonFormsRenderers];
|
||||
|
||||
/** Called when the user clicks the "Apply" button */
|
||||
const submitSettingsUpdate = async () => {
|
||||
console.log('[ConnectSettings] trying to update settings to', formState.value);
|
||||
await mutateSettings({ input: formState.value });
|
||||
await refetch();
|
||||
};
|
||||
|
||||
/** Called whenever a JSONForms form control changes */
|
||||
@@ -112,6 +116,7 @@ const onChange = ({ data }: { data: Record<string, unknown> }) => {
|
||||
<div class="mt-6 grid grid-cols-settings gap-y-6 items-baseline">
|
||||
<div class="text-sm text-end">
|
||||
<p v-if="isUpdating">Applying Settings...</p>
|
||||
<p v-else-if="restartRequired">The API will restart after settings are applied.</p>
|
||||
</div>
|
||||
<div class="col-start-2 ml-10 space-y-4">
|
||||
<BrandButton
|
||||
@@ -123,7 +128,6 @@ const onChange = ({ data }: { data: Record<string, unknown> }) => {
|
||||
>
|
||||
Apply
|
||||
</BrandButton>
|
||||
|
||||
<p v-if="mutateSettingsError" class="text-sm text-unraid-red-500">
|
||||
✕ Error: {{ mutateSettingsError.message }}
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user