Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions api/justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
28 changes: 22 additions & 6 deletions api/src/unraid-api/graph/connect/connect-settings.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -89,20 +91,30 @@ 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[]) {
const { store } = await import('@app/store/index.js');
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> {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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: {
Expand All @@ -273,6 +288,7 @@ export class ConnectSettingsService {
label: 'Enable Developer Sandbox:',
options: {
toggle: true,
description: sandbox ? description : undefined,
},
},
],
Expand Down
18 changes: 15 additions & 3 deletions api/src/unraid-api/graph/connect/connect.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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()
Expand Down
15 changes: 13 additions & 2 deletions api/src/unraid-api/graph/connect/connect.service.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
4 changes: 4 additions & 0 deletions unraid-ui/src/forms/Switch.vue
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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"
Expand Down
18 changes: 11 additions & 7 deletions web/components/ConnectSettings/ConnectSettings.ce.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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,
});
});

/**--------------------------------------------
Expand All @@ -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 */
Expand Down Expand Up @@ -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
Expand All @@ -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>
Expand Down
Loading