mirror of
https://github.com/unraid/api.git
synced 2026-01-01 06:01:18 -06:00
feat: download fixtures from the web
This commit is contained in:
3
api/.gitignore
vendored
3
api/.gitignore
vendored
@@ -80,3 +80,6 @@ deploy/*
|
|||||||
|
|
||||||
# IDE Settings Files
|
# IDE Settings Files
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
# Downloaded Fixtures (For File Modifications)
|
||||||
|
src/unraid-api/unraid-file-modifier/modifications/__fixtures__/downloaded/*
|
||||||
|
|||||||
4
api/.prettierignore
Normal file
4
api/.prettierignore
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
!src/*
|
||||||
|
|
||||||
|
# Downloaded Fixtures (For File Modifications)
|
||||||
|
src/unraid-api/unraid-file-modifier/modifications/__fixtures__/downloaded/*
|
||||||
@@ -1,13 +1,9 @@
|
|||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
import { readFile, writeFile, access, unlink } from 'fs/promises';
|
|
||||||
import { constants } from 'fs';
|
import { constants } from 'fs';
|
||||||
import { join, dirname, basename } from 'path';
|
import { access, readFile, unlink, writeFile } from 'fs/promises';
|
||||||
import { applyPatch, parsePatch, reversePatch } from 'diff';
|
import { basename, dirname, join } from 'path';
|
||||||
|
|
||||||
export interface PatchResult {
|
import { applyPatch, parsePatch, reversePatch } from 'diff';
|
||||||
targetFile: string;
|
|
||||||
patch: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ShouldApplyWithReason {
|
export interface ShouldApplyWithReason {
|
||||||
shouldApply: boolean;
|
shouldApply: boolean;
|
||||||
@@ -17,11 +13,12 @@ export interface ShouldApplyWithReason {
|
|||||||
// Convert interface to abstract class with default implementations
|
// Convert interface to abstract class with default implementations
|
||||||
export abstract class FileModification {
|
export abstract class FileModification {
|
||||||
abstract id: string;
|
abstract id: string;
|
||||||
|
public abstract readonly filePath: string;
|
||||||
|
|
||||||
protected constructor(protected readonly logger: Logger) {}
|
protected constructor(protected readonly logger: Logger) {}
|
||||||
|
|
||||||
// This is the main method that child classes need to implement
|
// This is the main method that child classes need to implement
|
||||||
protected abstract generatePatch(): Promise<PatchResult>;
|
protected abstract generatePatch(): Promise<string>;
|
||||||
|
|
||||||
private getPatchFilePath(targetFile: string): string {
|
private getPatchFilePath(targetFile: string): string {
|
||||||
const dir = dirname(targetFile);
|
const dir = dirname(targetFile);
|
||||||
@@ -29,9 +26,9 @@ export abstract class FileModification {
|
|||||||
return join(dir, filename);
|
return join(dir, filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async savePatch(patchResult: PatchResult): Promise<void> {
|
private async savePatch(patchResult: string): Promise<void> {
|
||||||
const patchFile = this.getPatchFilePath(patchResult.targetFile);
|
const patchFile = this.getPatchFilePath(this.filePath);
|
||||||
await writeFile(patchFile, patchResult.patch, 'utf8');
|
await writeFile(patchFile, patchResult, 'utf8');
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadSavedPatch(targetFile: string): Promise<string | null> {
|
private async loadSavedPatch(targetFile: string): Promise<string | null> {
|
||||||
@@ -44,38 +41,72 @@ export abstract class FileModification {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default implementation of apply that uses the patch
|
private async getPregeneratedPatch(): Promise<string | null> {
|
||||||
async apply(): Promise<void> {
|
const patchResults = await import.meta.glob('./modifications/patches/*.patch', {
|
||||||
const patchResult = await this.generatePatch();
|
query: '?raw',
|
||||||
const { targetFile, patch } = patchResult;
|
import: 'default',
|
||||||
const currentContent = await readFile(targetFile, 'utf8');
|
});
|
||||||
const parsedPatch = parsePatch(patch)[0];
|
|
||||||
const results = applyPatch(currentContent, parsedPatch);
|
if (patchResults[`./modifications/patches/${this.id}.patch`]) {
|
||||||
if (results === false) {
|
const loader = Object.values(patchResults)[0];
|
||||||
throw new Error(`Failed to apply patch to ${targetFile}`);
|
const fileContents = await loader();
|
||||||
|
this.logger.debug(`Loaded pregenerated patch for ${this.id}`);
|
||||||
|
if (typeof fileContents !== 'string') {
|
||||||
|
this.logger.error('Invalid patch format on patch: ' + this.id);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return fileContents;
|
||||||
}
|
}
|
||||||
|
|
||||||
await writeFile(targetFile, results);
|
return null;
|
||||||
await this.savePatch(patchResult);
|
}
|
||||||
|
|
||||||
|
private async applyPatch(patchContents: string): Promise<void> {
|
||||||
|
const currentContent = await readFile(this.filePath, 'utf8');
|
||||||
|
const parsedPatch = parsePatch(patchContents)[0];
|
||||||
|
const results = applyPatch(currentContent, parsedPatch);
|
||||||
|
if (results === false) {
|
||||||
|
throw new Error(`Failed to apply patch to ${this.filePath}`);
|
||||||
|
}
|
||||||
|
await writeFile(this.filePath, results);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default implementation of apply that uses the patch
|
||||||
|
async apply(): Promise<void> {
|
||||||
|
// First attempt to apply the patch that was generated
|
||||||
|
const staticPatch = await this.getPregeneratedPatch();
|
||||||
|
if (staticPatch) {
|
||||||
|
try {
|
||||||
|
await this.applyPatch(staticPatch);
|
||||||
|
await this.savePatch(staticPatch);
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(
|
||||||
|
`Failed to apply static patch to ${this.filePath}, continuing with dynamic patch`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const patchContents = await this.generatePatch();
|
||||||
|
await this.applyPatch(patchContents);
|
||||||
|
await this.savePatch(patchContents);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update rollback to use the shared utility
|
// Update rollback to use the shared utility
|
||||||
async rollback(): Promise<void> {
|
async rollback(): Promise<void> {
|
||||||
const { targetFile } = await this.generatePatch();
|
|
||||||
let patch: string;
|
let patch: string;
|
||||||
|
|
||||||
// Try to load saved patch first
|
// Try to load saved patch first
|
||||||
const savedPatch = await this.loadSavedPatch(targetFile);
|
const savedPatch = await this.loadSavedPatch(this.filePath);
|
||||||
if (savedPatch) {
|
if (savedPatch) {
|
||||||
this.logger.debug(`Using saved patch file for ${this.id}`);
|
this.logger.debug(`Using saved patch file for ${this.id}`);
|
||||||
patch = savedPatch;
|
patch = savedPatch;
|
||||||
} else {
|
} else {
|
||||||
this.logger.debug(`No saved patch found for ${this.id}, generating new patch`);
|
this.logger.debug(`No saved patch found for ${this.id}, generating new patch`);
|
||||||
const patchResult = await this.generatePatch();
|
const patchContents = await this.generatePatch();
|
||||||
patch = patchResult.patch;
|
patch = patchContents;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentContent = await readFile(targetFile, 'utf8');
|
const currentContent = await readFile(this.filePath, 'utf8');
|
||||||
const parsedPatch = parsePatch(patch)[0];
|
const parsedPatch = parsePatch(patch)[0];
|
||||||
|
|
||||||
if (!parsedPatch || !parsedPatch.hunks || parsedPatch.hunks.length === 0) {
|
if (!parsedPatch || !parsedPatch.hunks || parsedPatch.hunks.length === 0) {
|
||||||
@@ -86,14 +117,14 @@ export abstract class FileModification {
|
|||||||
const results = applyPatch(currentContent, reversedPatch);
|
const results = applyPatch(currentContent, reversedPatch);
|
||||||
|
|
||||||
if (results === false) {
|
if (results === false) {
|
||||||
throw new Error(`Failed to rollback patch from ${targetFile}`);
|
throw new Error(`Failed to rollback patch from ${this.filePath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await writeFile(targetFile, results);
|
await writeFile(this.filePath, results);
|
||||||
|
|
||||||
// Clean up the patch file after successful rollback
|
// Clean up the patch file after successful rollback
|
||||||
try {
|
try {
|
||||||
const patchFile = this.getPatchFilePath(targetFile);
|
const patchFile = this.getPatchFilePath(this.filePath);
|
||||||
await access(patchFile, constants.W_OK);
|
await access(patchFile, constants.W_OK);
|
||||||
await unlink(patchFile);
|
await unlink(patchFile);
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,286 +0,0 @@
|
|||||||
Menu="UserPreferences"
|
|
||||||
Type="xmenu"
|
|
||||||
Title="Notification Settings"
|
|
||||||
Icon="icon-notifications"
|
|
||||||
Tag="phone-square"
|
|
||||||
---
|
|
||||||
<?PHP
|
|
||||||
/* Copyright 2005-2023, Lime Technology
|
|
||||||
* Copyright 2012-2023, Bergware International.
|
|
||||||
* Copyright 2012, Andrew Hamer-Adams, http://www.pixeleyes.co.nz.
|
|
||||||
*
|
|
||||||
* This program is free software; you can redistribute it and/or
|
|
||||||
* modify it under the terms of the GNU General Public License version 2,
|
|
||||||
* as published by the Free Software Foundation.
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in
|
|
||||||
* all copies or substantial portions of the Software.
|
|
||||||
*/
|
|
||||||
?>
|
|
||||||
<?
|
|
||||||
$events = explode('|', $notify['events'] ?? '');
|
|
||||||
$disabled = $notify['system'] ? '' : 'disabled';
|
|
||||||
?>
|
|
||||||
<script>
|
|
||||||
function prepareNotify(form) {
|
|
||||||
form.entity.value = form.normal1.checked | form.warning1.checked | form.alert1.checked;
|
|
||||||
form.normal.value = form.normal1.checked*1 + form.normal2.checked*2 + form.normal3.checked*4;
|
|
||||||
form.warning.value = form.warning1.checked*1 + form.warning2.checked*2 + form.warning3.checked*4;
|
|
||||||
form.alert.value = form.alert1.checked*1 + form.alert2.checked*2 + form.alert3.checked*4;
|
|
||||||
form.unraid.value = form.unraid1.checked*1 + form.unraid2.checked*2 + form.unraid3.checked*4;
|
|
||||||
form.plugin.value = form.plugin1.checked*1 + form.plugin2.checked*2 + form.plugin3.checked*4;
|
|
||||||
form.docker_notify.value = form.docker_notify1.checked*1 + form.docker_notify2.checked*2 + form.docker_notify3.checked*4;
|
|
||||||
form.language_notify.value = form.language_notify1.checked*1 + form.language_notify2.checked*2 + form.language_notify3.checked*4;
|
|
||||||
form.report.value = form.report1.checked*1 + form.report2.checked*2 + form.report3.checked*4;
|
|
||||||
form.normal1.disabled = true;
|
|
||||||
form.normal2.disabled = true;
|
|
||||||
form.normal3.disabled = true;
|
|
||||||
form.warning1.disabled = true;
|
|
||||||
form.warning2.disabled = true;
|
|
||||||
form.warning3.disabled = true;
|
|
||||||
form.alert1.disabled = true;
|
|
||||||
form.alert2.disabled = true;
|
|
||||||
form.alert3.disabled = true;
|
|
||||||
form.unraid1.disabled = true;
|
|
||||||
form.unraid2.disabled = true;
|
|
||||||
form.unraid3.disabled = true;
|
|
||||||
form.plugin1.disabled = true;
|
|
||||||
form.plugin2.disabled = true;
|
|
||||||
form.plugin3.disabled = true;
|
|
||||||
form.docker_notify1.disabled = true;
|
|
||||||
form.docker_notify2.disabled = true;
|
|
||||||
form.docker_notify3.disabled = true;
|
|
||||||
form.language_notify1.disabled = true;
|
|
||||||
form.language_notify2.disabled = true;
|
|
||||||
form.language_notify3.disabled = true;
|
|
||||||
form.report1.disabled = true;
|
|
||||||
form.report2.disabled = true;
|
|
||||||
form.report3.disabled = true;
|
|
||||||
}
|
|
||||||
function prepareSystem(index) {
|
|
||||||
if (index==0) $('.checkbox').attr('disabled','disabled'); else $('.checkbox').removeAttr('disabled');
|
|
||||||
}
|
|
||||||
function prepareTitle() {
|
|
||||||
var title = '_(Available notifications)_:';
|
|
||||||
$('#unraidTitle,#pluginTitle,#dockerTitle,#languageTitle,#reportTitle').html(' ');
|
|
||||||
if ($('.unraid').is(':visible')) {$('#unraidTitle').html(title); return;}
|
|
||||||
if ($('.plugin').is(':visible')) {$('#pluginTitle').html(title); return;}
|
|
||||||
if ($('.docker').is(':visible')) {$('#dockerTitle').html(title); return;}
|
|
||||||
if ($('.language').is(':visible')) {$('#languageTitle').html(title); return;}
|
|
||||||
if ($('.report').is(':visible')) {$('#reportTitle').html(title); return;}
|
|
||||||
}
|
|
||||||
function prepareUnraid(value) {
|
|
||||||
if (value=='') $('.unraid').hide(); else $('.unraid').show();
|
|
||||||
prepareTitle();
|
|
||||||
}
|
|
||||||
function preparePlugin(value) {
|
|
||||||
if (value=='') $('.plugin').hide(); else $('.plugin').show();
|
|
||||||
prepareTitle();
|
|
||||||
}
|
|
||||||
function prepareDocker(value) {
|
|
||||||
if (value=='') $('.docker').hide(); else $('.docker').show();
|
|
||||||
prepareTitle();
|
|
||||||
}
|
|
||||||
function prepareLanguage(value) {
|
|
||||||
if (value=='') $('.language').hide(); else $('.language').show();
|
|
||||||
prepareTitle();
|
|
||||||
}
|
|
||||||
function prepareReport(value) {
|
|
||||||
if (value=='') $('.report').hide(); else $('.report').show();
|
|
||||||
prepareTitle();
|
|
||||||
}
|
|
||||||
$(function(){
|
|
||||||
prepareUnraid(document.notify_settings.unraidos.value);
|
|
||||||
preparePlugin(document.notify_settings.version.value);
|
|
||||||
prepareDocker(document.notify_settings.docker_update.value);
|
|
||||||
prepareLanguage(document.notify_settings.language_update.value);
|
|
||||||
prepareReport(document.notify_settings.status.value);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<form markdown="1" name="notify_settings" method="POST" action="/update.php" target="progressFrame" onsubmit="prepareNotify(this)">
|
|
||||||
<input type="hidden" name="#file" value="dynamix/dynamix.cfg">
|
|
||||||
<input type="hidden" name="#section" value="notify">
|
|
||||||
<input type="hidden" name="#command" value="/webGui/scripts/notify">
|
|
||||||
<input type="hidden" name="#arg[1]" value="cron-init">
|
|
||||||
<input type="hidden" name="entity">
|
|
||||||
<input type="hidden" name="normal">
|
|
||||||
<input type="hidden" name="warning">
|
|
||||||
<input type="hidden" name="alert">
|
|
||||||
<input type="hidden" name="unraid">
|
|
||||||
<input type="hidden" name="plugin">
|
|
||||||
<input type="hidden" name="docker_notify">
|
|
||||||
<input type="hidden" name="language_notify">
|
|
||||||
<input type="hidden" name="report">
|
|
||||||
_(Notifications display)_:
|
|
||||||
: <select class="a" name="display">
|
|
||||||
<?=mk_option($notify['display'], "0", _("Detailed"))?>
|
|
||||||
<?=mk_option($notify['display'], "1", _("Summarized"))?>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
:notifications_display_help:
|
|
||||||
|
|
||||||
_(Display position)_:
|
|
||||||
: <select name="position" class="a">
|
|
||||||
<?=mk_option($notify['position'], "top-left", _("top-left"))?>
|
|
||||||
<?=mk_option($notify['position'], "top-right", _("top-right"))?>
|
|
||||||
<?=mk_option($notify['position'], "bottom-left", _("bottom-left"))?>
|
|
||||||
<?=mk_option($notify['position'], "bottom-right", _("bottom-right"))?>
|
|
||||||
<?=mk_option($notify['position'], "center", _("center"))?>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
:notifications_display_position_help:
|
|
||||||
|
|
||||||
_(Auto-close)_ (_(seconds)_):
|
|
||||||
: <input type="number" name="life" class="a" min="0" max="60" value="<?=$notify['life']?>"> _(a value of zero means no automatic closure)_
|
|
||||||
|
|
||||||
:notifications_auto_close_help:
|
|
||||||
|
|
||||||
_(Date format)_:
|
|
||||||
: <select name="date" class="a">
|
|
||||||
<?=mk_option($notify['date'], "d-m-Y", _("DD-MM-YYYY"))?>
|
|
||||||
<?=mk_option($notify['date'], "m-d-Y", _("MM-DD-YYYY"))?>
|
|
||||||
<?=mk_option($notify['date'], "Y-m-d", _("YYYY-MM-DD"))?>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
:notifications_date_format_help:
|
|
||||||
|
|
||||||
_(Time format)_:
|
|
||||||
: <select name="time" class="a">
|
|
||||||
<?=mk_option($notify['time'], "h:i A", _("12 hours"))?>
|
|
||||||
<?=mk_option($notify['time'], "H:i", _("24 hours"))?>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
:notifications_time_format_help:
|
|
||||||
|
|
||||||
_(Store notifications to flash)_:
|
|
||||||
: <select name="path" class="a">
|
|
||||||
<?=mk_option($notify['path'], "/tmp/notifications", _("No"))?>
|
|
||||||
<?=mk_option($notify['path'], "/boot/config/plugins/dynamix/notifications", _("Yes"))?>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
:notifications_store_flash_help:
|
|
||||||
|
|
||||||
_(System notifications)_:
|
|
||||||
: <select name="system" class="a" onchange="prepareSystem(this.selectedIndex)">
|
|
||||||
<?=mk_option($notify['system'], "", _("Disabled"))?>
|
|
||||||
<?=mk_option($notify['system'], "*/1 * * * *", _("Enabled"))?>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
:notifications_system_help:
|
|
||||||
|
|
||||||
_(Unraid OS update notification)_:
|
|
||||||
: <select name="unraidos" class="a" onchange="prepareUnraid(this.value)">
|
|
||||||
<?=mk_option($notify['unraidos'], "", _("Never check"))?>
|
|
||||||
<?=mk_option($notify['unraidos'], "11 */6 * * *", _("Check four times a day"))?>
|
|
||||||
<?=mk_option($notify['unraidos'], "11 0,12 * * *", _("Check twice a day"))?>
|
|
||||||
<?=mk_option($notify['unraidos'], "11 0 * * *", _("Check once a day"))?>
|
|
||||||
<?=mk_option($notify['unraidos'], "11 0 * * 1", _("Check once a week"))?>
|
|
||||||
<?=mk_option($notify['unraidos'], "11 0 1 * *", _("Check once a month"))?>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
:notifications_os_update_help:
|
|
||||||
|
|
||||||
_(Plugins update notification)_:
|
|
||||||
: <select name="version" class="a" onchange="preparePlugin(this.value)">
|
|
||||||
<?=mk_option($notify['version'], "", _("Never check"))?>
|
|
||||||
<?=mk_option($notify['version'], "10 */6 * * *", _("Check four times a day"))?>
|
|
||||||
<?=mk_option($notify['version'], "10 0,12 * * *", _("Check twice a day"))?>
|
|
||||||
<?=mk_option($notify['version'], "10 0 * * *", _("Check once a day"))?>
|
|
||||||
<?=mk_option($notify['version'], "10 0 * * 1", _("Check once a week"))?>
|
|
||||||
<?=mk_option($notify['version'], "10 0 1 * *", _("Check once a month"))?>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
:notifications_plugins_update_help:
|
|
||||||
|
|
||||||
_(Docker update notification)_:
|
|
||||||
: <select name="docker_update" class="a" onchange="prepareDocker(this.value)">
|
|
||||||
<?=mk_option($notify['docker_update'], "", _("Never check"))?>
|
|
||||||
<?=mk_option($notify['docker_update'], "10 */6 * * *", _("Check four times a day"))?>
|
|
||||||
<?=mk_option($notify['docker_update'], "10 0,12 * * *", _("Check twice a day"))?>
|
|
||||||
<?=mk_option($notify['docker_update'], "10 0 * * *", _("Check once a day"))?>
|
|
||||||
<?=mk_option($notify['docker_update'], "10 0 * * 1", _("Check once a week"))?>
|
|
||||||
<?=mk_option($notify['docker_update'], "10 0 1 * *", _("Check once a month"))?>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
:notifications_docker_update_help:
|
|
||||||
|
|
||||||
_(Language update notification)_:
|
|
||||||
: <select name="language_update" class="a" onchange="prepareLanguage(this.value)">
|
|
||||||
<?=mk_option($notify['language_update'], "", _("Never check"))?>
|
|
||||||
<?=mk_option($notify['language_update'], "10 */6 * * *", _("Check four times a day"))?>
|
|
||||||
<?=mk_option($notify['language_update'], "10 0,12 * * *", _("Check twice a day"))?>
|
|
||||||
<?=mk_option($notify['language_update'], "10 0 * * *", _("Check once a day"))?>
|
|
||||||
<?=mk_option($notify['language_update'], "10 0 * * 1", _("Check once a week"))?>
|
|
||||||
<?=mk_option($notify['language_update'], "10 0 1 * *", _("Check once a month"))?>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
_(Array status notification)_:
|
|
||||||
: <select name="status" class="a" onchange="prepareReport(this.value)">
|
|
||||||
<?=mk_option($notify['status'], "", _("Never send"))?>
|
|
||||||
<?=mk_option($notify['status'], "20 * * * *", _("Send every hour"))?>
|
|
||||||
<?=mk_option($notify['status'], "20 */2 * * *", _("Send every two hours"))?>
|
|
||||||
<?=mk_option($notify['status'], "20 */6 * * *", _("Send four times a day"))?>
|
|
||||||
<?=mk_option($notify['status'], "20 */8 * * *", _("Send three times a day"))?>
|
|
||||||
<?=mk_option($notify['status'], "20 0,12 * * *", _("Send twice a day"))?>
|
|
||||||
<?=mk_option($notify['status'], "20 0 * * *", _("Send once a day"))?>
|
|
||||||
<?=mk_option($notify['status'], "20 0 * * 1", _("Send once a week"))?>
|
|
||||||
<?=mk_option($notify['status'], "20 0 1 * *", _("Send once a month"))?>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
:notifications_array_status_help:
|
|
||||||
|
|
||||||
<span id="unraidTitle" class="unraid" style="display:none"> </span>
|
|
||||||
: <span class="unraid" style="display:none"><span class="a">_(Unraid OS update)_</span>
|
|
||||||
<input type="checkbox" name="unraid1"<?=($notify['unraid'] & 1)==1 ? ' checked' : ''?>>_(Browser)_
|
|
||||||
<input type="checkbox" name="unraid2"<?=($notify['unraid'] & 2)==2 ? ' checked' : ''?>>_(Email)_
|
|
||||||
<input type="checkbox" name="unraid3"<?=($notify['unraid'] & 4)==4 ? ' checked' : ''?>>_(Agents)_ </span>
|
|
||||||
|
|
||||||
<span id="pluginTitle" class="plugin" style="display:none"> </span>
|
|
||||||
: <span class="plugin" style="display:none"><span class="a">_(Plugins update)_</span>
|
|
||||||
<input type="checkbox" name="plugin1"<?=($notify['plugin'] & 1)==1 ? ' checked' : ''?>>_(Browser)_
|
|
||||||
<input type="checkbox" name="plugin2"<?=($notify['plugin'] & 2)==2 ? ' checked' : ''?>>_(Email)_
|
|
||||||
<input type="checkbox" name="plugin3"<?=($notify['plugin'] & 4)==4 ? ' checked' : ''?>>_(Agents)_ </span>
|
|
||||||
|
|
||||||
<span id="dockerTitle" class="docker" style="display:none"> </span>
|
|
||||||
: <span class="docker" style="display:none"><span class="a">_(Docker update)_</span>
|
|
||||||
<input type="checkbox" name="docker_notify1"<?=($notify['docker_notify'] & 1)==1 ? ' checked' : ''?>>_(Browser)_
|
|
||||||
<input type="checkbox" name="docker_notify2"<?=($notify['docker_notify'] & 2)==2 ? ' checked' : ''?>>_(Email)_
|
|
||||||
<input type="checkbox" name="docker_notify3"<?=($notify['docker_notify'] & 4)==4 ? ' checked' : ''?>>_(Agents)_ </span>
|
|
||||||
|
|
||||||
<span id="languageTitle" class="language" style="display:none"> </span>
|
|
||||||
: <span class="language" style="display:none"><span class="a">_(Language update)_</span>
|
|
||||||
<input type="checkbox" name="language_notify1"<?=($notify['language_notify'] & 1)==1 ? ' checked' : ''?>>_(Browser)_
|
|
||||||
<input type="checkbox" name="language_notify2"<?=($notify['language_notify'] & 2)==2 ? ' checked' : ''?>>_(Email)_
|
|
||||||
<input type="checkbox" name="language_notify3"<?=($notify['language_notify'] & 4)==4 ? ' checked' : ''?>>_(Agents)_ </span>
|
|
||||||
|
|
||||||
<span id="reportTitle" class="report" style="display:none"> </span>
|
|
||||||
: <span class="report" style="display:none"><span class="a">_(Array status)_</span>
|
|
||||||
<input type="checkbox" name="report1"<?=($notify['report'] & 1)==1 ? ' checked' : ''?>>_(Browser)_
|
|
||||||
<input type="checkbox" name="report2"<?=($notify['report'] & 2)==2 ? ' checked' : ''?>>_(Email)_
|
|
||||||
<input type="checkbox" name="report3"<?=($notify['report'] & 4)==4 ? ' checked' : ''?>>_(Agents)_ </span>
|
|
||||||
|
|
||||||
:notifications_agent_selection_help:
|
|
||||||
|
|
||||||
_(Notification entity)_:
|
|
||||||
: <span class="a">_(Notices)_</span>
|
|
||||||
<input type="checkbox" class="checkbox" name="normal1"<?=($notify['normal'] & 1)==1 ? " checked $disabled" : $disabled?>>_(Browser)_
|
|
||||||
<input type="checkbox" class="checkbox" name="normal2"<?=($notify['normal'] & 2)==2 ? " checked $disabled" : $disabled?>>_(Email)_
|
|
||||||
<input type="checkbox" class="checkbox" name="normal3"<?=($notify['normal'] & 4)==4 ? " checked $disabled" : $disabled?>>_(Agents)_
|
|
||||||
|
|
||||||
|
|
||||||
: <span class="a">_(Warnings)_</span>
|
|
||||||
<input type="checkbox" class="checkbox" name="warning1"<?=($notify['warning'] & 1)==1 ? " checked $disabled" : $disabled?>>_(Browser)_
|
|
||||||
<input type="checkbox" class="checkbox" name="warning2"<?=($notify['warning'] & 2)==2 ? " checked $disabled" : $disabled?>>_(Email)_
|
|
||||||
<input type="checkbox" class="checkbox" name="warning3"<?=($notify['warning'] & 4)==4 ? " checked $disabled" : $disabled?>>_(Agents)_
|
|
||||||
|
|
||||||
|
|
||||||
: <span class="a">_(Alerts)_</span>
|
|
||||||
<input type="checkbox" class="checkbox" name="alert1"<?=($notify['alert'] & 1)==1 ? " checked $disabled" : $disabled?>>_(Browser)_
|
|
||||||
<input type="checkbox" class="checkbox" name="alert2"<?=($notify['alert'] & 2)==2 ? " checked $disabled" : $disabled?>>_(Email)_
|
|
||||||
<input type="checkbox" class="checkbox" name="alert3"<?=($notify['alert'] & 4)==4 ? " checked $disabled" : $disabled?>>_(Agents)_
|
|
||||||
|
|
||||||
:notifications_classification_help:
|
|
||||||
|
|
||||||
<input type="submit" name="#default" value="_(Default)_">
|
|
||||||
: <input type="submit" name="#apply" value="_(Apply)_" disabled><input type="button" value="_(Done)_" onclick="done()">
|
|
||||||
</form>
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
import { readFile } from 'fs/promises';
|
import { readFile, writeFile } from 'fs/promises';
|
||||||
import { resolve } from 'path';
|
import { basename, resolve } from 'path';
|
||||||
import { describe, expect, test } from 'vitest';
|
import { describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
import DefaultPageLayoutModification from '@app/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification';
|
import DefaultPageLayoutModification from '@app/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification';
|
||||||
@@ -9,32 +9,33 @@ import { FileModification } from '@app/unraid-api/unraid-file-modifier/file-modi
|
|||||||
import SSOFileModification from '@app/unraid-api/unraid-file-modifier/modifications/sso.modification';
|
import SSOFileModification from '@app/unraid-api/unraid-file-modifier/modifications/sso.modification';
|
||||||
|
|
||||||
interface ModificationTestCase {
|
interface ModificationTestCase {
|
||||||
name: string;
|
|
||||||
ModificationClass: new (logger: Logger) => FileModification;
|
ModificationClass: new (logger: Logger) => FileModification;
|
||||||
fileName: string;
|
fileUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const testCases: ModificationTestCase[] = [
|
const testCases: ModificationTestCase[] = [
|
||||||
{
|
{
|
||||||
name: 'DefaultPageLayout.php',
|
|
||||||
ModificationClass: DefaultPageLayoutModification,
|
ModificationClass: DefaultPageLayoutModification,
|
||||||
fileName: 'DefaultPageLayout.php'
|
fileUrl: 'https://github.com/unraid/webgui/raw/refs/heads/master/emhttp/plugins/dynamix/include/DefaultPageLayout.php',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Notifications.page',
|
|
||||||
ModificationClass: NotificationsPageModification,
|
ModificationClass: NotificationsPageModification,
|
||||||
fileName: 'Notifications.page'
|
fileUrl: "https://github.com/unraid/webgui/raw/refs/heads/master/emhttp/plugins/dynamix/Notifications.page",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '.login.php',
|
fileUrl: 'https://github.com/unraid/webgui/raw/refs/heads/master/emhttp/plugins/dynamix/include/.login.php',
|
||||||
ModificationClass: SSOFileModification,
|
ModificationClass: SSOFileModification,
|
||||||
fileName: '.login.php'
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
async function testModification(testCase: ModificationTestCase) {
|
async function testModification(testCase: ModificationTestCase) {
|
||||||
const path = resolve(__dirname, `../__fixtures__/${testCase.fileName}`);
|
// First download the file from Github
|
||||||
const fileContent = await readFile(path, 'utf-8');
|
const fileName = basename(testCase.fileUrl);
|
||||||
|
|
||||||
|
const path = resolve(__dirname, `../__fixtures__/downloaded/${fileName}`);
|
||||||
|
const fileContent = await fetch(testCase.fileUrl).then(response => response.text());
|
||||||
|
await writeFile(path, fileContent);
|
||||||
|
|
||||||
expect(fileContent.length).toBeGreaterThan(0);
|
expect(fileContent.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
const logger = new Logger();
|
const logger = new Logger();
|
||||||
@@ -44,23 +45,23 @@ async function testModification(testCase: ModificationTestCase) {
|
|||||||
|
|
||||||
// @ts-ignore - Ignore for testing purposes
|
// @ts-ignore - Ignore for testing purposes
|
||||||
const patch = await patcher.generatePatch();
|
const patch = await patcher.generatePatch();
|
||||||
|
|
||||||
// Test patch matches snapshot
|
// Test patch matches snapshot
|
||||||
await expect(patch.patch).toMatchFileSnapshot(
|
await expect(patch).toMatchFileSnapshot(
|
||||||
`snapshots/${testCase.fileName}.snapshot.patch`
|
`../patches/${patcher.id}.patch`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Apply patch and verify modified file
|
// Apply patch and verify modified file
|
||||||
await patcher.apply();
|
await patcher.apply();
|
||||||
await expect(await readFile(path, 'utf-8')).toMatchFileSnapshot(
|
await expect(await readFile(path, 'utf-8')).toMatchFileSnapshot(
|
||||||
`snapshots/${testCase.fileName}.modified.snapshot.php`
|
`snapshots/${fileName}.modified.snapshot.php`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Rollback and verify original state
|
// Rollback and verify original state
|
||||||
await patcher.rollback();
|
await patcher.rollback();
|
||||||
const revertedContent = await readFile(path, 'utf-8');
|
const revertedContent = await readFile(path, 'utf-8');
|
||||||
await expect(revertedContent).toMatchFileSnapshot(
|
await expect(revertedContent).toMatchFileSnapshot(
|
||||||
`snapshots/${testCase.fileName}.original.php`
|
`snapshots/${fileName}.original.php`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -200,11 +200,11 @@ function settab(tab) {
|
|||||||
$.cookie('one','tab1');
|
$.cookie('one','tab1');
|
||||||
<?endif;?>
|
<?endif;?>
|
||||||
<?break;?>
|
<?break;?>
|
||||||
<?case'Cache':case'Data':case'Flash':case'Parity':?>
|
<?case'Cache':case'Data':case'Device':case'Flash':case'Parity':?>
|
||||||
$.cookie('one',tab);
|
$.cookie('one',tab);
|
||||||
<?break;?>
|
<?break;?>
|
||||||
<?default:?>
|
<?default:?>
|
||||||
$.cookie(($.cookie('one')==null?'tab':'one'),tab);
|
$.cookie('one',tab);
|
||||||
<?endswitch;?>
|
<?endswitch;?>
|
||||||
}
|
}
|
||||||
function done(key) {
|
function done(key) {
|
||||||
@@ -577,8 +577,26 @@ function flashReport() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
$(function() {
|
$(function() {
|
||||||
var tab = $.cookie('one')||$.cookie('tab')||'tab1';
|
let tab;
|
||||||
if (tab=='tab0') tab = 'tab'+$('input[name$="tabs"]').length; else if ($('#'+tab).length==0) {initab(); tab = 'tab1';}
|
<?switch ($myPage['name']):?>
|
||||||
|
<?case'Main':?>
|
||||||
|
tab = $.cookie('tab')||'tab1';
|
||||||
|
<?break;?>
|
||||||
|
<?case'Cache':case'Data':case'Device':case'Flash':case'Parity':?>
|
||||||
|
tab = $.cookie('one')||'tab1';
|
||||||
|
<?break;?>
|
||||||
|
<?default:?>
|
||||||
|
tab = $.cookie('one')||'tab1';
|
||||||
|
<?endswitch;?>
|
||||||
|
/* Check if the tab is 'tab0' */
|
||||||
|
if (tab === 'tab0') {
|
||||||
|
/* Set tab to the last available tab based on input[name$="tabs"] length */
|
||||||
|
tab = 'tab' + $('input[name$="tabs"]').length;
|
||||||
|
} else if ($('#' + tab).length === 0) {
|
||||||
|
/* If the tab element does not exist, initialize a tab and set to 'tab1' */
|
||||||
|
initab();
|
||||||
|
tab = 'tab1';
|
||||||
|
}
|
||||||
$('#'+tab).attr('checked', true);
|
$('#'+tab).attr('checked', true);
|
||||||
updateTime();
|
updateTime();
|
||||||
$.jGrowl.defaults.closeTemplate = '<i class="fa fa-close"></i>';
|
$.jGrowl.defaults.closeTemplate = '<i class="fa fa-close"></i>';
|
||||||
@@ -693,7 +711,7 @@ if (isset($myPage['Load']) && $myPage['Load']>0) echo "\n<script>timers.reload =
|
|||||||
echo "<div class='tabs'>";
|
echo "<div class='tabs'>";
|
||||||
$tab = 1;
|
$tab = 1;
|
||||||
$pages = [];
|
$pages = [];
|
||||||
if (!empty($myPage['text'])) $pages[$myPage['name']] = $myPage;
|
if (!empty($myPage['text']) && page_enabled($myPage)) $pages[$myPage['name']] = $myPage;
|
||||||
if (_var($myPage,'Type')=='xmenu') $pages = array_merge($pages, find_pages($myPage['name']));
|
if (_var($myPage,'Type')=='xmenu') $pages = array_merge($pages, find_pages($myPage['name']));
|
||||||
if (isset($myPage['Tabs'])) $display['tabs'] = strtolower($myPage['Tabs'])=='true' ? 0 : 1;
|
if (isset($myPage['Tabs'])) $display['tabs'] = strtolower($myPage['Tabs'])=='true' ? 0 : 1;
|
||||||
$tabbed = $display['tabs']==0 && count($pages)>1;
|
$tabbed = $display['tabs']==0 && count($pages)>1;
|
||||||
@@ -701,7 +719,7 @@ $tabbed = $display['tabs']==0 && count($pages)>1;
|
|||||||
foreach ($pages as $page) {
|
foreach ($pages as $page) {
|
||||||
$close = false;
|
$close = false;
|
||||||
if (isset($page['Title'])) {
|
if (isset($page['Title'])) {
|
||||||
eval("\$title=\"".htmlspecialchars($page['Title'])."\";");
|
eval("\$title=\"{$page['Title']}\";");
|
||||||
if ($tabbed) {
|
if ($tabbed) {
|
||||||
echo "<div class='tab'><input type='radio' id='tab{$tab}' name='tabs' onclick='settab(this.id)'><label for='tab{$tab}'>";
|
echo "<div class='tab'><input type='radio' id='tab{$tab}' name='tabs' onclick='settab(this.id)'><label for='tab{$tab}'>";
|
||||||
echo tab_title($title,$page['root'],_var($page,'Tag',false));
|
echo tab_title($title,$page['root'],_var($page,'Tag',false));
|
||||||
@@ -718,7 +736,7 @@ foreach ($pages as $page) {
|
|||||||
if (isset($page['Type']) && $page['Type']=='menu') {
|
if (isset($page['Type']) && $page['Type']=='menu') {
|
||||||
$pgs = find_pages($page['name']);
|
$pgs = find_pages($page['name']);
|
||||||
foreach ($pgs as $pg) {
|
foreach ($pgs as $pg) {
|
||||||
@eval("\$title=\"".htmlspecialchars($pg['Title'])."\";");
|
@eval("\$title=\"{$pg['Title']}\";");
|
||||||
$icon = _var($pg,'Icon',"<i class='icon-app PanelIcon'></i>");
|
$icon = _var($pg,'Icon',"<i class='icon-app PanelIcon'></i>");
|
||||||
if (substr($icon,-4)=='.png') {
|
if (substr($icon,-4)=='.png') {
|
||||||
$root = $pg['root'];
|
$root = $pg['root'];
|
||||||
@@ -1189,4 +1207,3 @@ $('body').on("click","a,.ca_href", function(e) {
|
|||||||
<unraid-toaster rich-colors close-button position="<?= ($notify['position'] === 'center') ? 'top-center' : $notify['position'] ?>"></unraid-toaster>
|
<unraid-toaster rich-colors close-button position="<?= ($notify['position'] === 'center') ? 'top-center' : $notify['position'] ?>"></unraid-toaster>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
@@ -200,11 +200,11 @@ function settab(tab) {
|
|||||||
$.cookie('one','tab1');
|
$.cookie('one','tab1');
|
||||||
<?endif;?>
|
<?endif;?>
|
||||||
<?break;?>
|
<?break;?>
|
||||||
<?case'Cache':case'Data':case'Flash':case'Parity':?>
|
<?case'Cache':case'Data':case'Device':case'Flash':case'Parity':?>
|
||||||
$.cookie('one',tab);
|
$.cookie('one',tab);
|
||||||
<?break;?>
|
<?break;?>
|
||||||
<?default:?>
|
<?default:?>
|
||||||
$.cookie(($.cookie('one')==null?'tab':'one'),tab);
|
$.cookie('one',tab);
|
||||||
<?endswitch;?>
|
<?endswitch;?>
|
||||||
}
|
}
|
||||||
function done(key) {
|
function done(key) {
|
||||||
@@ -586,8 +586,26 @@ function flashReport() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
$(function() {
|
$(function() {
|
||||||
var tab = $.cookie('one')||$.cookie('tab')||'tab1';
|
let tab;
|
||||||
if (tab=='tab0') tab = 'tab'+$('input[name$="tabs"]').length; else if ($('#'+tab).length==0) {initab(); tab = 'tab1';}
|
<?switch ($myPage['name']):?>
|
||||||
|
<?case'Main':?>
|
||||||
|
tab = $.cookie('tab')||'tab1';
|
||||||
|
<?break;?>
|
||||||
|
<?case'Cache':case'Data':case'Device':case'Flash':case'Parity':?>
|
||||||
|
tab = $.cookie('one')||'tab1';
|
||||||
|
<?break;?>
|
||||||
|
<?default:?>
|
||||||
|
tab = $.cookie('one')||'tab1';
|
||||||
|
<?endswitch;?>
|
||||||
|
/* Check if the tab is 'tab0' */
|
||||||
|
if (tab === 'tab0') {
|
||||||
|
/* Set tab to the last available tab based on input[name$="tabs"] length */
|
||||||
|
tab = 'tab' + $('input[name$="tabs"]').length;
|
||||||
|
} else if ($('#' + tab).length === 0) {
|
||||||
|
/* If the tab element does not exist, initialize a tab and set to 'tab1' */
|
||||||
|
initab();
|
||||||
|
tab = 'tab1';
|
||||||
|
}
|
||||||
$('#'+tab).attr('checked', true);
|
$('#'+tab).attr('checked', true);
|
||||||
updateTime();
|
updateTime();
|
||||||
$.jGrowl.defaults.closeTemplate = '<i class="fa fa-close"></i>';
|
$.jGrowl.defaults.closeTemplate = '<i class="fa fa-close"></i>';
|
||||||
@@ -702,7 +720,7 @@ if (isset($myPage['Load']) && $myPage['Load']>0) echo "\n<script>timers.reload =
|
|||||||
echo "<div class='tabs'>";
|
echo "<div class='tabs'>";
|
||||||
$tab = 1;
|
$tab = 1;
|
||||||
$pages = [];
|
$pages = [];
|
||||||
if (!empty($myPage['text'])) $pages[$myPage['name']] = $myPage;
|
if (!empty($myPage['text']) && page_enabled($myPage)) $pages[$myPage['name']] = $myPage;
|
||||||
if (_var($myPage,'Type')=='xmenu') $pages = array_merge($pages, find_pages($myPage['name']));
|
if (_var($myPage,'Type')=='xmenu') $pages = array_merge($pages, find_pages($myPage['name']));
|
||||||
if (isset($myPage['Tabs'])) $display['tabs'] = strtolower($myPage['Tabs'])=='true' ? 0 : 1;
|
if (isset($myPage['Tabs'])) $display['tabs'] = strtolower($myPage['Tabs'])=='true' ? 0 : 1;
|
||||||
$tabbed = $display['tabs']==0 && count($pages)>1;
|
$tabbed = $display['tabs']==0 && count($pages)>1;
|
||||||
@@ -710,7 +728,7 @@ $tabbed = $display['tabs']==0 && count($pages)>1;
|
|||||||
foreach ($pages as $page) {
|
foreach ($pages as $page) {
|
||||||
$close = false;
|
$close = false;
|
||||||
if (isset($page['Title'])) {
|
if (isset($page['Title'])) {
|
||||||
eval("\$title=\"".htmlspecialchars($page['Title'])."\";");
|
eval("\$title=\"{$page['Title']}\";");
|
||||||
if ($tabbed) {
|
if ($tabbed) {
|
||||||
echo "<div class='tab'><input type='radio' id='tab{$tab}' name='tabs' onclick='settab(this.id)'><label for='tab{$tab}'>";
|
echo "<div class='tab'><input type='radio' id='tab{$tab}' name='tabs' onclick='settab(this.id)'><label for='tab{$tab}'>";
|
||||||
echo tab_title($title,$page['root'],_var($page,'Tag',false));
|
echo tab_title($title,$page['root'],_var($page,'Tag',false));
|
||||||
@@ -727,7 +745,7 @@ foreach ($pages as $page) {
|
|||||||
if (isset($page['Type']) && $page['Type']=='menu') {
|
if (isset($page['Type']) && $page['Type']=='menu') {
|
||||||
$pgs = find_pages($page['name']);
|
$pgs = find_pages($page['name']);
|
||||||
foreach ($pgs as $pg) {
|
foreach ($pgs as $pg) {
|
||||||
@eval("\$title=\"".htmlspecialchars($pg['Title'])."\";");
|
@eval("\$title=\"{$pg['Title']}\";");
|
||||||
$icon = _var($pg,'Icon',"<i class='icon-app PanelIcon'></i>");
|
$icon = _var($pg,'Icon',"<i class='icon-app PanelIcon'></i>");
|
||||||
if (substr($icon,-4)=='.png') {
|
if (substr($icon,-4)=='.png') {
|
||||||
$root = $pg['root'];
|
$root = $pg['root'];
|
||||||
@@ -1205,4 +1223,3 @@ $('body').on("click","a,.ca_href", function(e) {
|
|||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
Index: /app/src/unraid-api/unraid-file-modifier/modifications/__fixtures__/DefaultPageLayout.php
|
|
||||||
===================================================================
|
|
||||||
--- /app/src/unraid-api/unraid-file-modifier/modifications/__fixtures__/DefaultPageLayout.php
|
|
||||||
+++ /app/src/unraid-api/unraid-file-modifier/modifications/__fixtures__/DefaultPageLayout.php
|
|
||||||
@@ -557,14 +557,5 @@
|
|
||||||
$.post('/webGui/include/Notify.php',{cmd:'get',csrf_token:csrf_token},function(msg) {
|
|
||||||
$.each($.parseJSON(msg), function(i, notify){
|
|
||||||
- $.jGrowl(notify.subject+'<br>'+notify.description,{
|
|
||||||
- group: notify.importance,
|
|
||||||
- header: notify.event+': '+notify.timestamp,
|
|
||||||
- theme: notify.file,
|
|
||||||
- sticky: true,
|
|
||||||
- beforeOpen: function(e,m,o){if ($('div.jGrowl-notification').hasClass(notify.file)) return(false);},
|
|
||||||
- afterOpen: function(e,m,o){if (notify.link) $(e).css('cursor','pointer');},
|
|
||||||
- click: function(e,m,o){if (notify.link) location.replace(notify.link);},
|
|
||||||
- close: function(e,m,o){$.post('/webGui/include/Notify.php',{cmd:'archive',file:notify.file,csrf_token:csrf_token});}
|
|
||||||
- });
|
|
||||||
+
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -680,6 +671,6 @@
|
|
||||||
}
|
|
||||||
|
|
||||||
-echo "<div class='nav-user show'><a id='board' href='#' class='hand'><b id='bell' class='icon-u-bell system'></b></a></div>";
|
|
||||||
|
|
||||||
+
|
|
||||||
if ($themes2) echo "</div>";
|
|
||||||
echo "</div></div>";
|
|
||||||
@@ -886,20 +877,12 @@
|
|
||||||
<?if ($notify['display']==0):?>
|
|
||||||
if (notify.show) {
|
|
||||||
- $.jGrowl(notify.subject+'<br>'+notify.description,{
|
|
||||||
- group: notify.importance,
|
|
||||||
- header: notify.event+': '+notify.timestamp,
|
|
||||||
- theme: notify.file,
|
|
||||||
- beforeOpen: function(e,m,o){if ($('div.jGrowl-notification').hasClass(notify.file)) return(false);},
|
|
||||||
- afterOpen: function(e,m,o){if (notify.link) $(e).css('cursor','pointer');},
|
|
||||||
- click: function(e,m,o){if (notify.link) location.replace(notify.link);},
|
|
||||||
- close: function(e,m,o){$.post('/webGui/include/Notify.php',{cmd:'hide',file:"<?=$notify['path'].'/unread/'?>"+notify.file,csrf_token:csrf_token}<?if ($notify['life']==0):?>,function(){$.post('/webGui/include/Notify.php',{cmd:'archive',file:notify.file,csrf_token:csrf_token});}<?endif;?>);}
|
|
||||||
- });
|
|
||||||
+
|
|
||||||
}
|
|
||||||
<?endif;?>
|
|
||||||
});
|
|
||||||
- $('#bell').removeClass('red-orb yellow-orb green-orb').prop('title',"<?=_('Alerts')?> ["+bell1+']\n'+"<?=_('Warnings')?> ["+bell2+']\n'+"<?=_('Notices')?> ["+bell3+']');
|
|
||||||
- if (bell1) $('#bell').addClass('red-orb'); else
|
|
||||||
- if (bell2) $('#bell').addClass('yellow-orb'); else
|
|
||||||
- if (bell3) $('#bell').addClass('green-orb');
|
|
||||||
+
|
|
||||||
+
|
|
||||||
+
|
|
||||||
+
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
@@ -1204,4 +1187,5 @@
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
+<unraid-toaster rich-colors close-button position="<?= ($notify['position'] === 'center') ? 'top-center' : $notify['position'] ?>"></unraid-toaster>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -6,7 +6,6 @@ import { createPatch } from 'diff';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
FileModification,
|
FileModification,
|
||||||
PatchResult,
|
|
||||||
ShouldApplyWithReason,
|
ShouldApplyWithReason,
|
||||||
} from '@app/unraid-api/unraid-file-modifier/file-modification';
|
} from '@app/unraid-api/unraid-file-modifier/file-modification';
|
||||||
|
|
||||||
@@ -26,7 +25,7 @@ export default class AuthRequestModification extends FileModification {
|
|||||||
super(logger);
|
super(logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async generatePatch(): Promise<PatchResult> {
|
protected async generatePatch(): Promise<string> {
|
||||||
const JS_FILES = await getJsFiles(WEB_COMPS_DIR);
|
const JS_FILES = await getJsFiles(WEB_COMPS_DIR);
|
||||||
this.logger.debug(`Found ${JS_FILES.length} .js files in ${WEB_COMPS_DIR}`);
|
this.logger.debug(`Found ${JS_FILES.length} .js files in ${WEB_COMPS_DIR}`);
|
||||||
|
|
||||||
@@ -54,10 +53,7 @@ export default class AuthRequestModification extends FileModification {
|
|||||||
context: 3,
|
context: 3,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return patch;
|
||||||
targetFile: AUTH_REQUEST_FILE,
|
|
||||||
patch,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async shouldApply(): Promise<ShouldApplyWithReason> {
|
async shouldApply(): Promise<ShouldApplyWithReason> {
|
||||||
|
|||||||
@@ -5,13 +5,12 @@ import { createPatch } from 'diff';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
FileModification,
|
FileModification,
|
||||||
PatchResult,
|
|
||||||
ShouldApplyWithReason,
|
ShouldApplyWithReason,
|
||||||
} from '@app/unraid-api/unraid-file-modifier/file-modification';
|
} from '@app/unraid-api/unraid-file-modifier/file-modification';
|
||||||
|
|
||||||
export default class DefaultPageLayoutModification extends FileModification {
|
export default class DefaultPageLayoutModification extends FileModification {
|
||||||
id: string = 'default-page-layout';
|
id: string = 'default-page-layout';
|
||||||
private readonly filePath: string =
|
public readonly filePath: string =
|
||||||
'/usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php';
|
'/usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php';
|
||||||
|
|
||||||
constructor(logger: Logger) {
|
constructor(logger: Logger) {
|
||||||
@@ -47,7 +46,7 @@ export default class DefaultPageLayoutModification extends FileModification {
|
|||||||
return transformers.reduce((content, fn) => fn(content), fileContent);
|
return transformers.reduce((content, fn) => fn(content), fileContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async generatePatch(): Promise<PatchResult> {
|
protected async generatePatch(): Promise<string> {
|
||||||
const fileContent = await readFile(this.filePath, 'utf-8');
|
const fileContent = await readFile(this.filePath, 'utf-8');
|
||||||
|
|
||||||
|
|
||||||
@@ -57,10 +56,7 @@ export default class DefaultPageLayoutModification extends FileModification {
|
|||||||
context: 2,
|
context: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return patch;
|
||||||
targetFile: this.filePath,
|
|
||||||
patch,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async shouldApply(): Promise<ShouldApplyWithReason> {
|
async shouldApply(): Promise<ShouldApplyWithReason> {
|
||||||
|
|||||||
@@ -7,13 +7,12 @@ import { execa } from 'execa';
|
|||||||
import { fileExists } from '@app/core/utils/files/file-exists';
|
import { fileExists } from '@app/core/utils/files/file-exists';
|
||||||
import {
|
import {
|
||||||
FileModification,
|
FileModification,
|
||||||
PatchResult,
|
|
||||||
ShouldApplyWithReason,
|
ShouldApplyWithReason,
|
||||||
} from '@app/unraid-api/unraid-file-modifier/file-modification';
|
} from '@app/unraid-api/unraid-file-modifier/file-modification';
|
||||||
|
|
||||||
export class LogRotateModification extends FileModification {
|
export class LogRotateModification extends FileModification {
|
||||||
id: string = 'log-rotate';
|
id: string = 'log-rotate';
|
||||||
private readonly filePath: string = '/etc/logrotate.d/unraid-api' as const;
|
public readonly filePath: string = '/etc/logrotate.d/unraid-api' as const;
|
||||||
private readonly logRotateConfig: string = `
|
private readonly logRotateConfig: string = `
|
||||||
/var/log/unraid-api/*.log {
|
/var/log/unraid-api/*.log {
|
||||||
rotate 1
|
rotate 1
|
||||||
@@ -31,7 +30,7 @@ export class LogRotateModification extends FileModification {
|
|||||||
super(logger);
|
super(logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async generatePatch(): Promise<PatchResult> {
|
protected async generatePatch(): Promise<string> {
|
||||||
const currentContent = (await fileExists(this.filePath))
|
const currentContent = (await fileExists(this.filePath))
|
||||||
? await readFile(this.filePath, 'utf8')
|
? await readFile(this.filePath, 'utf8')
|
||||||
: '';
|
: '';
|
||||||
@@ -50,10 +49,7 @@ export class LogRotateModification extends FileModification {
|
|||||||
// After applying patch, ensure file permissions are correct
|
// After applying patch, ensure file permissions are correct
|
||||||
await execa('chown', ['root:root', this.filePath]).catch((err) => this.logger.error(err));
|
await execa('chown', ['root:root', this.filePath]).catch((err) => this.logger.error(err));
|
||||||
|
|
||||||
return {
|
return patch;
|
||||||
targetFile: this.filePath,
|
|
||||||
patch,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async shouldApply(): Promise<ShouldApplyWithReason> {
|
async shouldApply(): Promise<ShouldApplyWithReason> {
|
||||||
|
|||||||
@@ -5,19 +5,18 @@ import { createPatch } from 'diff';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
FileModification,
|
FileModification,
|
||||||
PatchResult,
|
|
||||||
ShouldApplyWithReason,
|
ShouldApplyWithReason,
|
||||||
} from '@app/unraid-api/unraid-file-modifier/file-modification';
|
} from '@app/unraid-api/unraid-file-modifier/file-modification';
|
||||||
|
|
||||||
export default class NotificationsPageModification extends FileModification {
|
export default class NotificationsPageModification extends FileModification {
|
||||||
id: string = 'Notifications.page';
|
id: string = 'notifications-page';
|
||||||
private readonly filePath: string = '/usr/local/emhttp/plugins/dynamix/Notifications.page';
|
public readonly filePath: string = '/usr/local/emhttp/plugins/dynamix/Notifications.page';
|
||||||
|
|
||||||
constructor(logger: Logger) {
|
constructor(logger: Logger) {
|
||||||
super(logger);
|
super(logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async generatePatch(): Promise<PatchResult> {
|
protected async generatePatch(): Promise<string> {
|
||||||
const fileContent = await readFile(this.filePath, 'utf-8');
|
const fileContent = await readFile(this.filePath, 'utf-8');
|
||||||
|
|
||||||
const newContent = NotificationsPageModification.applyToSource(fileContent);
|
const newContent = NotificationsPageModification.applyToSource(fileContent);
|
||||||
@@ -26,10 +25,7 @@ export default class NotificationsPageModification extends FileModification {
|
|||||||
context: 3,
|
context: 3,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return patch;
|
||||||
targetFile: this.filePath,
|
|
||||||
patch,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async shouldApply(): Promise<ShouldApplyWithReason> {
|
async shouldApply(): Promise<ShouldApplyWithReason> {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
Index: /app/src/unraid-api/unraid-file-modifier/modifications/__fixtures__/DefaultPageLayout.php
|
Index: /app/src/unraid-api/unraid-file-modifier/modifications/__fixtures__/downloaded/DefaultPageLayout.php
|
||||||
===================================================================
|
===================================================================
|
||||||
--- /app/src/unraid-api/unraid-file-modifier/modifications/__fixtures__/DefaultPageLayout.php
|
--- /app/src/unraid-api/unraid-file-modifier/modifications/__fixtures__/downloaded/DefaultPageLayout.php
|
||||||
+++ /app/src/unraid-api/unraid-file-modifier/modifications/__fixtures__/DefaultPageLayout.php
|
+++ /app/src/unraid-api/unraid-file-modifier/modifications/__fixtures__/downloaded/DefaultPageLayout.php
|
||||||
@@ -557,14 +557,5 @@
|
@@ -557,14 +557,5 @@
|
||||||
$.post('/webGui/include/Notify.php',{cmd:'get',csrf_token:csrf_token},function(msg) {
|
$.post('/webGui/include/Notify.php',{cmd:'get',csrf_token:csrf_token},function(msg) {
|
||||||
$.each($.parseJSON(msg), function(i, notify){
|
$.each($.parseJSON(msg), function(i, notify){
|
||||||
@@ -18,7 +18,7 @@ Index: /app/src/unraid-api/unraid-file-modifier/modifications/__fixtures__/Defau
|
|||||||
+
|
+
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -680,6 +671,6 @@
|
@@ -698,6 +689,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
-echo "<div class='nav-user show'><a id='board' href='#' class='hand'><b id='bell' class='icon-u-bell system'></b></a></div>";
|
-echo "<div class='nav-user show'><a id='board' href='#' class='hand'><b id='bell' class='icon-u-bell system'></b></a></div>";
|
||||||
@@ -26,7 +26,7 @@ Index: /app/src/unraid-api/unraid-file-modifier/modifications/__fixtures__/Defau
|
|||||||
+
|
+
|
||||||
if ($themes2) echo "</div>";
|
if ($themes2) echo "</div>";
|
||||||
echo "</div></div>";
|
echo "</div></div>";
|
||||||
@@ -886,20 +877,12 @@
|
@@ -904,20 +895,12 @@
|
||||||
<?if ($notify['display']==0):?>
|
<?if ($notify['display']==0):?>
|
||||||
if (notify.show) {
|
if (notify.show) {
|
||||||
- $.jGrowl(notify.subject+'<br>'+notify.description,{
|
- $.jGrowl(notify.subject+'<br>'+notify.description,{
|
||||||
@@ -52,7 +52,7 @@ Index: /app/src/unraid-api/unraid-file-modifier/modifications/__fixtures__/Defau
|
|||||||
+
|
+
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1204,4 +1187,5 @@
|
@@ -1222,4 +1205,5 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
+<unraid-toaster rich-colors close-button position="<?= ($notify['position'] === 'center') ? 'top-center' : $notify['position'] ?>"></unraid-toaster>
|
+<unraid-toaster rich-colors close-button position="<?= ($notify['position'] === 'center') ? 'top-center' : $notify['position'] ?>"></unraid-toaster>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
Index: /app/src/unraid-api/unraid-file-modifier/modifications/__fixtures__/Notifications.page
|
Index: /app/src/unraid-api/unraid-file-modifier/modifications/__fixtures__/downloaded/Notifications.page
|
||||||
===================================================================
|
===================================================================
|
||||||
--- /app/src/unraid-api/unraid-file-modifier/modifications/__fixtures__/Notifications.page
|
--- /app/src/unraid-api/unraid-file-modifier/modifications/__fixtures__/downloaded/Notifications.page
|
||||||
+++ /app/src/unraid-api/unraid-file-modifier/modifications/__fixtures__/Notifications.page
|
+++ /app/src/unraid-api/unraid-file-modifier/modifications/__fixtures__/downloaded/Notifications.page
|
||||||
@@ -135,23 +135,7 @@
|
@@ -135,23 +135,7 @@
|
||||||
|
|
||||||
:notifications_auto_close_help:
|
:notifications_auto_close_help:
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
Index: /app/src/unraid-api/unraid-file-modifier/modifications/__fixtures__/downloaded/.login.php
|
||||||
|
===================================================================
|
||||||
|
--- /app/src/unraid-api/unraid-file-modifier/modifications/__fixtures__/downloaded/.login.php original
|
||||||
|
+++ /app/src/unraid-api/unraid-file-modifier/modifications/__fixtures__/downloaded/.login.php modified
|
||||||
|
@@ -1,5 +1,33 @@
|
||||||
|
<?php
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+function verifyUsernamePasswordAndSSO(string $username, string $password): bool {
|
||||||
|
+ if ($username != "root") return false;
|
||||||
|
+
|
||||||
|
+ $output = exec("/usr/bin/getent shadow $username");
|
||||||
|
+ if ($output === false) return false;
|
||||||
|
+ $credentials = explode(":", $output);
|
||||||
|
+ $valid = password_verify($password, $credentials[1]);
|
||||||
|
+ if ($valid) {
|
||||||
|
+ return true;
|
||||||
|
+ }
|
||||||
|
+ // We may have an SSO token, attempt validation
|
||||||
|
+ if (strlen($password) > 800) {
|
||||||
|
+ $safePassword = escapeshellarg($password);
|
||||||
|
+ if (!preg_match('/^[A-Za-z0-9-_]+.[A-Za-z0-9-_]+.[A-Za-z0-9-_]+$/', $password)) {
|
||||||
|
+ my_logger("SSO Login Attempt Failed: Invalid token format");
|
||||||
|
+ return false;
|
||||||
|
+ }
|
||||||
|
+ $safePassword = escapeshellarg($password);
|
||||||
|
+ $response = exec("/usr/local/bin/unraid-api sso validate-token $safePassword", $output, $code);
|
||||||
|
+ my_logger("SSO Login Attempt: $response");
|
||||||
|
+ if ($code === 0 && $response && strpos($response, '"valid":true') !== false) {
|
||||||
|
+ return true;
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ return false;
|
||||||
|
+}
|
||||||
|
// Included in login.php
|
||||||
|
|
||||||
|
// Only start a session to check if they have a cookie that looks like our session
|
||||||
|
$server_name = strtok($_SERVER['HTTP_HOST'],":");
|
||||||
|
@@ -203,9 +231,9 @@
|
||||||
|
throw new Exception(_('Too many invalid login attempts'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bail if username + password combo doesn't work
|
||||||
|
- if (!verifyUsernamePassword($username, $password)) throw new Exception(_('Invalid username or password'));
|
||||||
|
+ if (!verifyUsernamePasswordAndSSO($username, $password)) throw new Exception(_('Invalid username or password'));
|
||||||
|
|
||||||
|
// Bail if we need a token but it's invalid
|
||||||
|
if (isWildcardCert() && $twoFactorRequired && !verifyTwoFactorToken($username, $token)) throw new Exception(_('Invalid 2FA token'));
|
||||||
|
|
||||||
|
@@ -537,8 +565,9 @@
|
||||||
|
document.body.appendChild(errorElement);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</form>
|
||||||
|
+<?php include "$docroot/plugins/dynamix.my.servers/include/sso-login.php"; ?>
|
||||||
|
|
||||||
|
<? if (($twoFactorRequired && !empty($token)) || !$twoFactorRequired) { ?>
|
||||||
|
<div class="js-addTimeout hidden">
|
||||||
|
<p class="error" style="padding-top:10px;"><?=_('Transparent 2FA Token timed out')?></p>
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
import type { Logger } from '@nestjs/common';
|
import type { Logger } from '@nestjs/common';
|
||||||
import { readFile } from 'node:fs/promises';
|
import { readFile } from 'node:fs/promises';
|
||||||
import { createPatch } from 'diff';
|
import { createPatch } from 'diff';
|
||||||
import { FileModification, PatchResult, ShouldApplyWithReason } from '@app/unraid-api/unraid-file-modifier/file-modification';
|
import { FileModification, ShouldApplyWithReason } from '@app/unraid-api/unraid-file-modifier/file-modification';
|
||||||
|
|
||||||
export default class SSOFileModification extends FileModification {
|
export default class SSOFileModification extends FileModification {
|
||||||
id: string = 'sso';
|
id: string = 'sso';
|
||||||
private readonly filePath: string = '/usr/local/emhttp/plugins/dynamix/include/.login.php';
|
public readonly filePath: string = '/usr/local/emhttp/plugins/dynamix/include/.login.php';
|
||||||
|
|
||||||
constructor(logger: Logger) {
|
constructor(logger: Logger) {
|
||||||
super(logger);
|
super(logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async generatePatch(): Promise<PatchResult> {
|
protected async generatePatch(): Promise<string> {
|
||||||
// Define the new PHP function to insert
|
// Define the new PHP function to insert
|
||||||
/* eslint-disable no-useless-escape */
|
/* eslint-disable no-useless-escape */
|
||||||
const newFunction = `
|
const newFunction = `
|
||||||
@@ -65,11 +65,7 @@ function verifyUsernamePasswordAndSSO(string $username, string $password): bool
|
|||||||
|
|
||||||
// Create and return the patch
|
// Create and return the patch
|
||||||
const patch = createPatch(this.filePath, originalContent, newContent, 'original', 'modified');
|
const patch = createPatch(this.filePath, originalContent, newContent, 'original', 'modified');
|
||||||
|
return patch;
|
||||||
return {
|
|
||||||
targetFile: this.filePath,
|
|
||||||
patch
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async shouldApply(): Promise<ShouldApplyWithReason> {
|
async shouldApply(): Promise<ShouldApplyWithReason> {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
FileModification,
|
FileModification,
|
||||||
PatchResult,
|
|
||||||
ShouldApplyWithReason,
|
ShouldApplyWithReason,
|
||||||
} from '@app/unraid-api/unraid-file-modifier/file-modification';
|
} from '@app/unraid-api/unraid-file-modifier/file-modification';
|
||||||
import { UnraidFileModificationService } from '@app/unraid-api/unraid-file-modifier/unraid-file-modifier.service';
|
import { UnraidFileModificationService } from '@app/unraid-api/unraid-file-modifier/unraid-file-modifier.service';
|
||||||
@@ -18,16 +17,14 @@ const ORIGINAL_CONTENT = 'original';
|
|||||||
|
|
||||||
class TestFileModification extends FileModification {
|
class TestFileModification extends FileModification {
|
||||||
id = 'test';
|
id = 'test';
|
||||||
|
public readonly filePath: string = FIXTURE_PATH;
|
||||||
|
|
||||||
constructor(logger: Logger) {
|
constructor(logger: Logger) {
|
||||||
super(logger);
|
super(logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async generatePatch(): Promise<PatchResult> {
|
protected async generatePatch(): Promise<string> {
|
||||||
return {
|
return createPatch('text-patch-file.txt', ORIGINAL_CONTENT, 'modified');
|
||||||
targetFile: FIXTURE_PATH,
|
|
||||||
patch: createPatch('text-patch-file.txt', ORIGINAL_CONTENT, 'modified'),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async shouldApply(): Promise<ShouldApplyWithReason> {
|
async shouldApply(): Promise<ShouldApplyWithReason> {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { defineConfig } from 'vitest/config';
|
|||||||
|
|
||||||
export default defineConfig(({ mode }): ViteUserConfig => {
|
export default defineConfig(({ mode }): ViteUserConfig => {
|
||||||
return {
|
return {
|
||||||
assetsInclude: ['src/**/*.graphql'],
|
assetsInclude: ['src/**/*.graphql', 'src/**/*.patch'],
|
||||||
plugins: [
|
plugins: [
|
||||||
tsconfigPaths(),
|
tsconfigPaths(),
|
||||||
nodeExternals(),
|
nodeExternals(),
|
||||||
|
|||||||
Reference in New Issue
Block a user