mirror of
https://github.com/unraid/api.git
synced 2026-05-07 23:51:40 -05: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:
+4
-4
@@ -9,8 +9,8 @@ default:
|
|||||||
pnpm run build
|
pnpm run build
|
||||||
|
|
||||||
# deploys to an unraid server
|
# deploys to an unraid server
|
||||||
@deploy:
|
@deploy remote:
|
||||||
./scripts/deploy-dev.sh
|
./scripts/deploy-dev.sh {{remote}}
|
||||||
|
|
||||||
# build & deploy
|
alias b := build
|
||||||
bd: build deploy
|
alias d := deploy
|
||||||
|
|||||||
@@ -61,8 +61,10 @@ export class ConnectSettingsService {
|
|||||||
/**
|
/**
|
||||||
* Syncs the settings to the store and writes the config to disk
|
* Syncs the settings to the store and writes the config to disk
|
||||||
* @param settings - The settings to sync
|
* @param settings - The settings to sync
|
||||||
|
* @returns true if a restart is required, false otherwise
|
||||||
*/
|
*/
|
||||||
async syncSettings(settings: Partial<ApiSettingsInput>) {
|
async syncSettings(settings: Partial<ApiSettingsInput>) {
|
||||||
|
let restartRequired = false;
|
||||||
const { getters } = await import('@app/store/index.js');
|
const { getters } = await import('@app/store/index.js');
|
||||||
const { nginx } = getters.emhttp();
|
const { nginx } = getters.emhttp();
|
||||||
if (settings.accessType === WAN_ACCESS_TYPE.DISABLED) {
|
if (settings.accessType === WAN_ACCESS_TYPE.DISABLED) {
|
||||||
@@ -89,10 +91,11 @@ export class ConnectSettingsService {
|
|||||||
await this.updateAllowedOrigins(settings.extraOrigins);
|
await this.updateAllowedOrigins(settings.extraOrigins);
|
||||||
}
|
}
|
||||||
if (typeof settings.sandbox === 'boolean') {
|
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');
|
const { writeConfigSync } = await import('@app/store/sync/config-disk-sync.js');
|
||||||
writeConfigSync('flash');
|
writeConfigSync('flash');
|
||||||
|
return restartRequired;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateAllowedOrigins(origins: string[]) {
|
private async updateAllowedOrigins(origins: string[]) {
|
||||||
@@ -100,9 +103,18 @@ export class ConnectSettingsService {
|
|||||||
store.dispatch(updateAllowedOrigins(origins));
|
store.dispatch(updateAllowedOrigins(origins));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async setSandboxMode(sandbox: boolean) {
|
/**
|
||||||
const { store } = await import('@app/store/index.js');
|
* Sets the sandbox mode and returns true if the mode was changed
|
||||||
store.dispatch(updateUserConfig({ local: { sandbox: sandbox ? 'yes' : 'no' } }));
|
* @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> {
|
private async updateRemoteAccess(input: SetupRemoteAccessInput): Promise<boolean> {
|
||||||
@@ -137,7 +149,7 @@ export class ConnectSettingsService {
|
|||||||
async buildSettingsSchema(): Promise<SettingSlice> {
|
async buildSettingsSchema(): Promise<SettingSlice> {
|
||||||
const slices = [
|
const slices = [
|
||||||
await this.remoteAccessSlice(),
|
await this.remoteAccessSlice(),
|
||||||
this.sandboxSlice(),
|
await this.sandboxSlice(),
|
||||||
this.flashBackupSlice(),
|
this.flashBackupSlice(),
|
||||||
// Because CORS is effectively disabled, this setting is no longer necessary
|
// Because CORS is effectively disabled, this setting is no longer necessary
|
||||||
// keeping it here for in case it needs to be re-enabled
|
// keeping it here for in case it needs to be re-enabled
|
||||||
@@ -257,7 +269,10 @@ export class ConnectSettingsService {
|
|||||||
/**
|
/**
|
||||||
* Developer sandbox settings slice
|
* 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 {
|
return {
|
||||||
properties: {
|
properties: {
|
||||||
sandbox: {
|
sandbox: {
|
||||||
@@ -273,6 +288,7 @@ export class ConnectSettingsService {
|
|||||||
label: 'Enable Developer Sandbox:',
|
label: 'Enable Developer Sandbox:',
|
||||||
options: {
|
options: {
|
||||||
toggle: true,
|
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 { store } from '@app/store/index.js';
|
||||||
import { setAllowedRemoteAccessUrl } from '@app/store/modules/dynamic-remote-access.js';
|
import { setAllowedRemoteAccessUrl } from '@app/store/modules/dynamic-remote-access.js';
|
||||||
import { ConnectSettingsService } from '@app/unraid-api/graph/connect/connect-settings.service.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')
|
@Resolver('Connect')
|
||||||
export class ConnectResolver implements ConnectResolvers {
|
export class ConnectResolver implements ConnectResolvers {
|
||||||
protected logger = new Logger(ConnectResolver.name);
|
protected logger = new Logger(ConnectResolver.name);
|
||||||
constructor(private readonly connectSettingsService: ConnectSettingsService) {}
|
constructor(
|
||||||
|
private readonly connectSettingsService: ConnectSettingsService,
|
||||||
|
private readonly connectService: ConnectService
|
||||||
|
) {}
|
||||||
|
|
||||||
@Query('connect')
|
@Query('connect')
|
||||||
@UsePermissions({
|
@UsePermissions({
|
||||||
@@ -62,8 +66,16 @@ export class ConnectResolver implements ConnectResolvers {
|
|||||||
})
|
})
|
||||||
public async updateApiSettings(@Args('input') settings: ApiSettingsInput) {
|
public async updateApiSettings(@Args('input') settings: ApiSettingsInput) {
|
||||||
this.logger.verbose(`Attempting to update API settings: ${JSON.stringify(settings, null, 2)}`);
|
this.logger.verbose(`Attempting to update API settings: ${JSON.stringify(settings, null, 2)}`);
|
||||||
await this.connectSettingsService.syncSettings(settings);
|
const restartRequired = await this.connectSettingsService.syncSettings(settings);
|
||||||
return this.connectSettingsService.getCurrentSettings();
|
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()
|
@ResolveField()
|
||||||
|
|||||||
@@ -1,4 +1,15 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { execa } from 'execa';
|
||||||
|
|
||||||
@Injectable()
|
@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">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
import { Switch as UuiSwitch } from '@/components/form/switch';
|
import { Switch as UuiSwitch } from '@/components/form/switch';
|
||||||
import { useJsonFormsControl } from '@jsonforms/vue';
|
import { useJsonFormsControl } from '@jsonforms/vue';
|
||||||
|
|
||||||
@@ -12,10 +14,12 @@ const { control, handleChange } = useJsonFormsControl(props);
|
|||||||
const onChange = (checked: boolean) => {
|
const onChange = (checked: boolean) => {
|
||||||
handleChange(control.value.path, checked);
|
handleChange(control.value.path, checked);
|
||||||
};
|
};
|
||||||
|
const description = computed(() => props.uischema.options?.description);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ControlLayout v-if="control.visible" :label="control.label" :errors="control.errors">
|
<ControlLayout v-if="control.visible" :label="control.label" :errors="control.errors">
|
||||||
|
<p v-if="description" v-html="description" class="mb-2"></p>
|
||||||
<UuiSwitch
|
<UuiSwitch
|
||||||
:id="control.id + '-input'"
|
:id="control.id + '-input'"
|
||||||
:name="control.path"
|
:name="control.path"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { useMutation, useQuery } from '@vue/apollo-composable';
|
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 { JsonForms } from '@jsonforms/vue';
|
||||||
|
|
||||||
import type { ConnectSettingsValues } from '~/composables/gql/graphql';
|
import type { ConnectSettingsValues } from '~/composables/gql/graphql';
|
||||||
@@ -17,7 +17,7 @@ import { getConnectSettingsForm, updateConnectSettings } from './graphql/setting
|
|||||||
*---------------------------------------------**/
|
*---------------------------------------------**/
|
||||||
|
|
||||||
const formState = ref<Partial<ConnectSettingsValues>>({});
|
const formState = ref<Partial<ConnectSettingsValues>>({});
|
||||||
const { result } = useQuery(getConnectSettingsForm);
|
const { result, refetch } = useQuery(getConnectSettingsForm);
|
||||||
const settings = computed(() => {
|
const settings = computed(() => {
|
||||||
if (!result.value) return;
|
if (!result.value) return;
|
||||||
return result.value?.connect.settings;
|
return result.value?.connect.settings;
|
||||||
@@ -27,6 +27,9 @@ watch(result, () => {
|
|||||||
const { __typename, ...initialValues } = result.value.connect.settings.values;
|
const { __typename, ...initialValues } = result.value.connect.settings.values;
|
||||||
formState.value = initialValues;
|
formState.value = initialValues;
|
||||||
});
|
});
|
||||||
|
const restartRequired = computed(() => {
|
||||||
|
return settings.value?.values.sandbox !== formState.value?.sandbox;
|
||||||
|
});
|
||||||
|
|
||||||
/**--------------------------------------------
|
/**--------------------------------------------
|
||||||
* Update Settings Actions
|
* Update Settings Actions
|
||||||
@@ -54,7 +57,9 @@ watchDebounced(
|
|||||||
|
|
||||||
// show a toast when the update is done
|
// show a toast when the update is done
|
||||||
onMutateSettingsDone(() => {
|
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,
|
trim: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderers = [
|
const renderers = [...jsonFormsRenderers];
|
||||||
...jsonFormsRenderers,
|
|
||||||
];
|
|
||||||
|
|
||||||
/** Called when the user clicks the "Apply" button */
|
/** Called when the user clicks the "Apply" button */
|
||||||
const submitSettingsUpdate = async () => {
|
const submitSettingsUpdate = async () => {
|
||||||
console.log('[ConnectSettings] trying to update settings to', formState.value);
|
console.log('[ConnectSettings] trying to update settings to', formState.value);
|
||||||
await mutateSettings({ input: formState.value });
|
await mutateSettings({ input: formState.value });
|
||||||
|
await refetch();
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Called whenever a JSONForms form control changes */
|
/** 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="mt-6 grid grid-cols-settings gap-y-6 items-baseline">
|
||||||
<div class="text-sm text-end">
|
<div class="text-sm text-end">
|
||||||
<p v-if="isUpdating">Applying Settings...</p>
|
<p v-if="isUpdating">Applying Settings...</p>
|
||||||
|
<p v-else-if="restartRequired">The API will restart after settings are applied.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-start-2 ml-10 space-y-4">
|
<div class="col-start-2 ml-10 space-y-4">
|
||||||
<BrandButton
|
<BrandButton
|
||||||
@@ -123,7 +128,6 @@ const onChange = ({ data }: { data: Record<string, unknown> }) => {
|
|||||||
>
|
>
|
||||||
Apply
|
Apply
|
||||||
</BrandButton>
|
</BrandButton>
|
||||||
|
|
||||||
<p v-if="mutateSettingsError" class="text-sm text-unraid-red-500">
|
<p v-if="mutateSettingsError" class="text-sm text-unraid-red-500">
|
||||||
✕ Error: {{ mutateSettingsError.message }}
|
✕ Error: {{ mutateSettingsError.message }}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user